Compare commits

..

11 Commits

Author SHA1 Message Date
2e665fd66e feat: SMP stage 1 (4 harts, per-hart guarded stacks, home-core affinity, IPIs, TLB shootdown, one temporary kernel lock), 4-core storm 2026-10-27 00:56:15 +01:00
b87a62475f feat(adapter): higher-half kernel (linked at PHYS_OFFSET, boot trampoline, no low mappings), fix SBI ecall clobbers 2026-10-27 00:25:20 +01:00
55758ae5b2 feat: kernel W^X (per-section perms, firmware unmapped, stack guard, boot check), halt on kernel faults, region W^X across processes 2026-10-27 00:13:00 +01:00
bafc18a5f8 feat(kernel): SystemControl cap for halt, board device table + boot caps to init, RegionAddress syscall, idle loop services irqs 2026-10-26 23:59:57 +01:00
130a82f50d feat(kernel): shared ABI decoder+proofs, no-panic gate, syscall storm, kani on shrunk kernel 2026-10-26 23:44:09 +01:00
154599e3af feat(kernel): derivation-tree revocation, region caps
- caps record parent (mint/delegate/ipc), CapRef w/ pid+slot gens
- Revoke = delete subtree, keep self; drop/death re-parents children
- CreateRegion/MapRegion; mapping lives only as long as its cap
- drop Permission::Revoke + object-wide revoke
- mint any cap (badge only ep/ntfn), process caps delegable
- kani: descendants == subtree; model-based fuzz vs spec
2026-10-26 20:41:03 +01:00
76f10ff1f1 feat(kernel): seL4-style IPC, notifications, status ABI
- badges + Mint, Call/Reply/ReplyRecv w/ gen-checked reply slot
- cap transfer (1/msg, copy, needs Delegate, rechecked at delivery)
- notifications: signal/wait/poll/bind, irq -> notification routing
- WaitAny/WaitForInterrupt removed; a0=status, ErrorCode
- libkos: single ecall primitive, compat wrappers + typed new api
- fix: trap return clobbered user t0 on every trap
- fix: wait queues FIFO, delegate keeps badge, fault cap checked
- docs/ipc.md spec, ipc tests, ipc fuzz, kani ntfn + waitqueue
2026-10-26 20:27:38 +01:00
d43421e33e feat(kernel): object lifetimes via slot generations
- SlotGenerations per table, never wraps, retires exhausted slots
- caps to dead/reused process, endpoint, region rejected
- endpoints owned, die w/ owner, waiters woken w/ error
- DropCap syscall + libkos wrapper
- irq table bounds-checked, routes dropped w/ thread
- self-kill terminates caller, exit_thread full cleanup
- sched run_now: switch to named thread, not queue head
- lifetime tests, fuzz kills/recreates, kani gen proofs
2026-10-26 19:54:23 +01:00
8d2c40cb1e feat(kernel): kernel-owned memory model
- FrameTable: per-frame owner/use, replaces PagePool + ProcessMemory
- Quota per process, page tables charged to owner
- Regions for shared mem + device MMIO, torn down w/ owner
- Platform: create/destroy AS, map/unmap, zero_frame; drop translate_va
- RequestPages/ReleasePages/MapPage(move) checked vs kernel records
- user window 0x1000..32MiB, private pts by construction
- boot loader + device setup go through kernel api
- kernel built in place, boot allocator bounded
- isolation tests, randomized invariant test, FrameTable kani proofs
- fixes: inflated release, unmapped release, dst va unchecked,
  heap overflow, unbounded pt alloc, unzeroed frames, double free
2026-10-26 19:40:50 +01:00
e931485ca7 fix: sched round-robin, cap slot OOB panic, clippy, make ci
- RunList -> intrusive FIFO, no starvation, no silent drop
- cap table sized to CapSlot range (kani caught OOB panic)
- snake food color test uses const
- kani cfg lint, RUST_MIN_STACK for big kernel struct in tests
2026-10-26 19:08:30 +01:00
1c04ddf909 style: cargo fmt kernel crates 2026-10-26 19:07:54 +01:00
138 changed files with 11996 additions and 3185 deletions

View File

@@ -1,3 +1,7 @@
[target.riscv64gc-unknown-none-elf] [target.riscv64gc-unknown-none-elf]
runner = "qemu-system-riscv64 -machine virt -nographic -bios default -kernel" runner = "qemu-system-riscv64 -machine virt -nographic -bios default -kernel"
rustflags = ["-C", "panic=abort"] rustflags = ["-C", "panic=abort"]
[env]
# Kernel tables are large fixed arrays; tests build `Kernel` by value.
RUST_MIN_STACK = "33554432"

1
Cargo.lock generated
View File

@@ -245,7 +245,6 @@ dependencies = [
name = "platform" name = "platform"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"domain",
"types", "types",
] ]

View File

@@ -123,3 +123,10 @@ panic = "abort"
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
# The randomized kernel invariant tests are too slow unoptimized.
[profile.test.package.kernel]
opt-level = 2
[profile.test.package.domain]
opt-level = 2

View File

@@ -1,10 +1,27 @@
.PHONY: check fmt clippy test kani build all image run .PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test
RISCV_TARGET = riscv64gc-unknown-none-elf RISCV_TARGET = riscv64gc-unknown-none-elf
KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv
# Harts QEMU boots; `make run-gui SMP=1` for a single core.
SMP ?= 4
KERNEL_CRATES = -p types -p domain -p kernel -p platform
all: check fmt clippy test all: check fmt clippy test
# Gate for kernel work: formatting, lints, unit tests, Kani proofs, RISC-V build.
ci: kernel-fmt kernel-clippy kernel-test kani build
kernel-fmt:
cargo fmt $(KERNEL_CRATES) -p adapter-riscv -- --check
kernel-clippy:
cargo clippy $(KERNEL_CRATES) --all-targets -- -D warnings
cargo clippy -p adapter-riscv --target $(RISCV_TARGET) --no-deps -- -D warnings
kernel-test:
cargo test $(KERNEL_CRATES)
check: check:
cargo check --workspace cargo check --workspace
@@ -17,14 +34,17 @@ clippy:
test: test:
cargo test --workspace cargo test --workspace
# CBMC can eat all RAM and take the desktop down with it; cap each run.
KANI = systemd-run --user --scope -q -p MemoryMax=12G -p MemorySwapMax=0 cargo kani
kani-types: kani-types:
cargo kani -p types $(KANI) -p types
kani-domain: kani-domain:
cargo kani -p domain $(KANI) -p domain
kani-kernel: kani-kernel:
cargo kani -p kernel $(KANI) -p kernel
kani: kani-types kani-domain kani-kernel kani: kani-types kani-domain kani-kernel
@@ -38,17 +58,17 @@ image: build
scripts/make-disk-image.sh scripts/make-disk-image.sh
run: build run: build
qemu-system-riscv64 -machine virt -nographic -bios default \ qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) -kernel $(KERNEL_BIN)
run-disk: image run-disk: image
qemu-system-riscv64 -machine virt -nographic -bios default \ qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 -device virtio-blk-device,drive=hd0
run-gui: image run-gui: image
qemu-system-riscv64 -machine virt -bios default \ qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \ -device virtio-blk-device,drive=hd0 \

View File

@@ -1,36 +1,60 @@
OUTPUT_ARCH(riscv) OUTPUT_ARCH(riscv)
ENTRY(_start)
/* The kernel runs in the upper half: linked at its physical load address
plus PHYS_OFFSET (mmu::PHYS_OFFSET), loaded at the physical address.
Firmware jumps to the physical entry with the MMU off; _start switches
to the high mapping before any Rust code runs. */
PHYS_OFFSET = 0xFFFFFFC000000000;
LOAD_ADDRESS = 0x80200000;
ENTRY(_start_phys)
/* Every boundary below is page aligned: the kernel maps each range with
its own permissions (W^X), see mmu::init_kernel_page_table. */
SECTIONS { SECTIONS {
. = 0x80200000; . = PHYS_OFFSET + LOAD_ADDRESS;
_text_start = .;
.text : { .text : AT(ADDR(.text) - PHYS_OFFSET) {
*(.text.entry) *(.text.entry)
*(.text .text.*) *(.text .text.*)
} }
. = ALIGN(4096); . = ALIGN(4096);
.rodata : { _text_end = .;
.rodata : AT(ADDR(.rodata) - PHYS_OFFSET) {
*(.rodata .rodata.*) *(.rodata .rodata.*)
*(.srodata .srodata.*)
}
.eh_frame : AT(ADDR(.eh_frame) - PHYS_OFFSET) {
*(.eh_frame)
} }
. = ALIGN(4096); . = ALIGN(4096);
.data : { _rodata_end = .;
.data : AT(ADDR(.data) - PHYS_OFFSET) {
*(.data .data.*) *(.data .data.*)
*(.sdata .sdata.*)
} }
. = ALIGN(4096); . = ALIGN(4096);
.bss : { .bss : AT(ADDR(.bss) - PHYS_OFFSET) {
_bss_start = .; _bss_start = .;
*(.bss .bss.*) *(.bss .bss.*)
*(.sbss .sbss.*) *(.sbss .sbss.*)
_bss_end = .; _bss_end = .;
} }
/* One kernel stack per hart (smp::MAX_HARTS, smp::STACK_SLOT): each
slot is an unmapped guard page, so overflowing a stack faults
instead of silently overwriting what lies below, then the stack. */
. = ALIGN(4096); . = ALIGN(4096);
_stack_bottom = .; _stacks_start = .;
. += 0x80000; . += 8 * (4096 + 0x80000);
_stack_top = .; _stacks_end = .;
_heap_start = .; _heap_start = .;
} }
_start_phys = _start - PHYS_OFFSET;

View File

@@ -8,3 +8,29 @@ pub const CLINT_BASE: usize = 0x02000000;
pub const USER_POOL_BASE: usize = 0x84000000; pub const USER_POOL_BASE: usize = 0x84000000;
pub const USER_POOL_PAGES: usize = 4096; pub const USER_POOL_PAGES: usize = 4096;
pub const TICK_INTERVAL: u64 = 10_000_000; pub const TICK_INTERVAL: u64 = 10_000_000;
/// QEMU virt's PLIC lines.
const UART_IRQ: u16 = 10;
const VIRTIO_IRQ_BASE: u16 = 1;
const VIRTIO_SLOT_SIZE: usize = 0x1000;
const VIRTIO_SLOTS: usize = VIRTIO_SIZE / VIRTIO_SLOT_SIZE;
/// Every device userspace may drive: the UART and each VirtIO MMIO slot.
/// The PLIC, CLINT and RAM stay the kernel's.
pub fn devices() -> Option<[kernel::BoardDevice; 1 + VIRTIO_SLOTS]> {
use types::{address::PhysicalAddress, bounded::PageCount, id::IrqId};
let one = PageCount::new(1)?;
let device = |base: usize, irq: u16| kernel::BoardDevice {
base: PhysicalAddress::from_raw(base),
pages: one,
irq: Some(IrqId::from_raw(irq)),
};
let mut all = [device(UART_BASE, UART_IRQ); 1 + VIRTIO_SLOTS];
for (i, d) in all.iter_mut().skip(1).enumerate() {
*d = device(
VIRTIO_BASE + i * VIRTIO_SLOT_SIZE,
VIRTIO_IRQ_BASE + i as u16,
);
}
Some(all)
}

View File

@@ -22,37 +22,85 @@ pub fn grant_all(
.with(types::permission::Permission::Read) .with(types::permission::Permission::Read)
.with(types::permission::Permission::Execute); .with(types::permission::Permission::Execute);
let storage_ep = kern().create_endpoint() let storage_ep = kern()
.create_endpoint(storage_pid)
.unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(storage_pid), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Process(storage_pid),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(storage_ep), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Endpoint(storage_ep),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(storage_pid, domain::resource::Resource::Endpoint(storage_ep), ipc_perms) kern()
.grant_capability(
storage_pid,
domain::resource::Resource::Endpoint(storage_ep),
ipc_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
let display_ep = kern().create_endpoint() let display_ep = kern()
.create_endpoint(display_pid)
.unwrap_or_else(|_| crate::halt(uart, "display endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "display endpoint failed"));
kern().grant_capability(display_pid, domain::resource::Resource::Endpoint(display_ep), ipc_perms) kern()
.grant_capability(
display_pid,
domain::resource::Resource::Endpoint(display_ep),
ipc_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(display_ep), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Endpoint(display_ep),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(display_pid), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Process(display_pid),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
let input_ep = kern().create_endpoint() let input_ep = kern()
.create_endpoint(input_pid)
.unwrap_or_else(|_| crate::halt(uart, "input endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "input endpoint failed"));
kern().grant_capability(input_pid, domain::resource::Resource::Endpoint(input_ep), ipc_perms) kern()
.grant_capability(
input_pid,
domain::resource::Resource::Endpoint(input_ep),
ipc_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Endpoint(input_ep), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Endpoint(input_ep),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
kern().grant_capability(init_pid, domain::resource::Resource::Process(input_pid), all_perms) kern()
.grant_capability(
init_pid,
domain::resource::Resource::Process(input_pid),
all_perms,
)
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
uart.write_str("k-os: capabilities granted\n"); uart.write_str("k-os: capabilities granted\n");

View File

@@ -1,6 +1,6 @@
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk; use crate::hw::virtio_blk::VirtioBlk;
use crate::{board, kernel_cell::KernelCell, mmu, platform, trap, KERNEL}; use crate::{KERNEL, board, mmu, platform, trap};
unsafe extern "C" { unsafe extern "C" {
static _bss_start: u8; static _bss_start: u8;
@@ -8,26 +8,66 @@ unsafe extern "C" {
static _heap_start: u8; static _heap_start: u8;
} }
pub fn init(uart: &Uart) -> &'static mut mmu::PageTable { pub fn init(uart: &Uart, boot_hart: usize) -> &'static mut mmu::PageTable {
zero_bss(); zero_bss();
let heap_start = unsafe { &_heap_start as *const u8 as usize }; let heap_start = unsafe { &_heap_start as *const u8 as usize };
mmu::init_allocator(heap_start); mmu::init_allocator(heap_start);
let kernel_pt = mmu::init_kernel_page_table(); let kernel_pt = mmu::init_kernel_page_table();
let harts = crate::smp::probe_harts();
if boot_hart >= harts {
crate::halt(uart, "boot hart outside the hart range");
}
crate::smp::init(
boot_hart,
harts,
mmu::virt_to_phys(kernel_pt as *const mmu::PageTable as usize),
);
mmu::activate(kernel_pt); mmu::activate(kernel_pt);
uart.write_str("k-os: MMU enabled (Sv39)\n"); uart.write_str("k-os: MMU enabled (Sv39)\n");
match mmu::check_wx(kernel_pt) {
Ok(code_pages) => {
uart.write_str("k-os: W^X verified, ");
uart.write_dec(code_pages);
uart.write_str(" code pages\n");
}
Err(v) => {
let (what, va) = match v {
mmu::page_table::WxViolation::WritableCode(va) => ("writable code", va),
mmu::page_table::WxViolation::ExecutableData(va) => ("executable data", va),
mmu::page_table::WxViolation::MappedHole(va) => ("mapped hole", va),
mmu::page_table::WxViolation::LowerHalf(va) => ("lower-half mapping", va),
};
uart.write_str("k-os: W^X violation: ");
uart.write_str(what);
uart.write_str(" at ");
uart.write_hex(va);
uart.put_char(b'\n');
crate::halt(uart, "kernel page table violates W^X");
}
}
trap::init(); trap::init();
crate::hw::timer::init(); crate::hw::timer::init();
crate::hw::plic::Plic::init(); crate::hw::plic::Plic::init();
uart.write_str("k-os: traps and timer initialized\n"); uart.write_str("k-os: traps and timer initialized\n");
let riscv_platform = platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize); if board::USER_POOL_PAGES > domain::memory::frame::MAX_FRAMES {
let pool = domain::memory::page_pool::PagePool::new( crate::halt(uart, "user pool larger than the frame table");
types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE), }
board::USER_POOL_PAGES, let riscv_platform =
); platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize, harts as u8);
KERNEL.init(kernel::Kernel::new(riscv_platform, pool)); // SAFETY: `init_in_place` writes every field of the kernel.
unsafe {
KERNEL.init_with(|slot| {
kernel::Kernel::init_in_place(
slot,
riscv_platform,
types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE),
board::USER_POOL_PAGES,
)
});
}
kernel_pt kernel_pt
} }

View File

@@ -1,84 +1,74 @@
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk; use crate::hw::virtio_blk::VirtioBlk;
use crate::{boot_fs, mmu}; use crate::{boot_fs, mmu};
use types::address::VirtualAddress;
use types::id::{ProcessId, ThreadId}; use types::id::{ProcessId, ThreadId};
use types::mapping::PagePerms;
const PAGE_SIZE: usize = mmu::PAGE_SIZE;
const USER_VA_BASE: usize = 0x10000; const USER_VA_BASE: usize = 0x10000;
/// The stack sits just below the image and stops short of the null page.
const USER_STACK_PAGES: usize = USER_VA_BASE / PAGE_SIZE - 1;
const MAX_FILE_SIZE: usize = 64 * 1024; const MAX_FILE_SIZE: usize = 64 * 1024;
pub struct LoadedProcess { pub struct LoadedProcess {
pub pid: ProcessId, pub pid: ProcessId,
pub entry: usize,
} }
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> { fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() } unsafe { &mut *crate::kernel_ptr() }
} }
pub fn load_init( /// Scratch space for reading ELF files, allocated once from the boot heap.
uart: &Uart, fn elf_buffer() -> &'static mut [u8] {
blk: &mut VirtioBlk, static mut BUF: usize = 0;
kernel_pt: &mmu::PageTable, // SAFETY: boot is single threaded; the buffer is kernel-only memory
) -> (LoadedProcess, ThreadId) { // from the boot allocator and is never handed to a process.
unsafe {
if BUF == 0 {
BUF = mmu::allocator::alloc_zeroed_page();
for _ in 1..MAX_FILE_SIZE / PAGE_SIZE {
mmu::allocator::alloc_zeroed_page();
}
}
core::slice::from_raw_parts_mut(BUF as *mut u8, MAX_FILE_SIZE)
}
}
pub fn load_init(uart: &Uart, blk: &mut VirtioBlk) -> (LoadedProcess, ThreadId) {
let (pid, tid) = kern() let (pid, tid) = kern()
.boot(types::bounded::Priority::DEFAULT, types::time::TimeSlice::DEFAULT) .boot(
types::bounded::Priority::DEFAULT,
types::time::TimeSlice::DEFAULT,
)
.unwrap_or_else(|_| crate::halt(uart, "boot failed")); .unwrap_or_else(|_| crate::halt(uart, "boot failed"));
let entry = load_elf(blk, kernel_pt, pid, b"init.elf"); let entry = load_elf(uart, blk, pid, b"init.elf");
if entry == 0 { crate::halt(uart, "init.elf not found"); }
setup_thread(tid, entry); setup_thread(tid, entry);
uart.write_str("k-os: init loaded\n"); uart.write_str("k-os: init loaded\n");
(LoadedProcess { pid, entry }, tid) (LoadedProcess { pid }, tid)
} }
pub fn load_server( pub fn load_server(
uart: &Uart, uart: &Uart,
blk: &mut VirtioBlk, blk: &mut VirtioBlk,
kernel_pt: &mmu::PageTable,
filename: &[u8], filename: &[u8],
label: &str, label: &str,
) -> LoadedProcess { ) -> LoadedProcess {
let pid = kern().create_process_internal() let pid = kern()
.create_process_internal()
.unwrap_or_else(|| crate::halt(uart, "process create failed")); .unwrap_or_else(|| crate::halt(uart, "process create failed"));
let entry = load_elf(blk, kernel_pt, pid, filename); let entry = load_elf(uart, blk, pid, filename);
if entry == 0 { crate::halt(uart, "elf not found"); } let tid = kern()
let tid = kern().spawn_thread_internal(pid) .spawn_thread_internal(pid)
.unwrap_or_else(|| crate::halt(uart, "thread spawn failed")); .unwrap_or_else(|| crate::halt(uart, "thread spawn failed"));
setup_thread(tid, entry); setup_thread(tid, entry);
uart.write_str("k-os: "); uart.write_str("k-os: ");
uart.write_str(label); uart.write_str(label);
uart.write_str(" loaded\n"); uart.write_str(" loaded\n");
LoadedProcess { pid, entry } LoadedProcess { pid }
}
pub fn enter_userspace(pid: ProcessId, entry: usize, shared_buf_va: usize) -> ! {
let pt_addr = kern().platform_mut().page_table_for(pid);
if pt_addr != 0 {
mmu::activate(unsafe { &*(pt_addr as *const mmu::PageTable) });
}
let kernel_sp: usize;
unsafe { core::arch::asm!("mv {0}, sp", out(reg) kernel_sp) };
crate::trap::set_kernel_stack(kernel_sp);
unsafe {
core::arch::asm!(
"csrc sstatus, {spp}",
"csrs sstatus, {spie}",
"csrw sepc, {entry}",
"mv sp, {stack}",
"mv a0, {arg}",
"sret",
spp = in(reg) 1usize << 8,
spie = in(reg) 1usize << 5,
entry = in(reg) entry,
stack = in(reg) USER_VA_BASE,
arg = in(reg) shared_buf_va,
options(noreturn),
);
}
} }
fn setup_thread(tid: ThreadId, entry: usize) { fn setup_thread(tid: ThreadId, entry: usize) {
@@ -89,88 +79,89 @@ fn setup_thread(tid: ThreadId, entry: usize) {
} }
} }
fn load_elf( /// Copies `data` into `pid`'s already mapped pages starting at `va`.
blk: &mut VirtioBlk, fn copy_to_user(pid: ProcessId, va: usize, data: &[u8]) -> bool {
kernel_pt: &mmu::PageTable, let mut done = 0;
pid: ProcessId, while done < data.len() {
filename: &[u8], let addr = va + done;
) -> usize { let page = addr & !(PAGE_SIZE - 1);
let buf_base = mmu::allocator::alloc_zeroed_page(); let offset = addr - page;
let file_pages = MAX_FILE_SIZE / 4096; let Some(pa) = kern().user_page_frame(pid, VirtualAddress::from_raw(page)) else {
for _ in 1..file_pages { return false;
mmu::allocator::alloc_zeroed_page(); };
let len = (PAGE_SIZE - offset).min(data.len() - done);
// SAFETY: `pa` is a pool frame the kernel just mapped for `pid`;
// pool frames are RAM, mapped at `phys_to_virt`.
unsafe {
core::ptr::copy_nonoverlapping(
data.as_ptr().add(done),
(mmu::phys_to_virt(pa.raw()) as *mut u8).add(offset),
len,
);
}
done += len;
} }
let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) }; true
}
fn segment_perms(flags: elf::SegmentFlags) -> Option<PagePerms> {
match (flags.write, flags.execute) {
(true, true) => None,
(false, true) => Some(PagePerms::ReadExecute),
(true, false) => Some(PagePerms::ReadWrite),
(false, false) => Some(PagePerms::ReadOnly),
}
}
/// Loads `filename` into `pid` through the kernel's own mapping API, so
/// every page is tracked, charged and checked like any other. Halts on any
/// malformed or overlapping image: boot servers are trusted to be sane.
fn load_elf(uart: &Uart, blk: &mut VirtioBlk, pid: ProcessId, filename: &[u8]) -> usize {
let buf = elf_buffer();
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0); let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0);
if file_size == 0 { return 0; } if file_size == 0 {
crate::halt(uart, "ELF not found");
let elf_info = match elf::parse(&buf[..file_size]) { }
Ok(info) => info, let file = &buf[..file_size];
Err(_) => return 0, let elf_info = elf::parse(file).unwrap_or_else(|_| crate::halt(uart, "bad ELF"));
};
let user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(0),
0,
types::address::VirtualAddress::from_raw(USER_VA_BASE),
);
for segment in elf_info.segments() { for segment in elf_info.segments() {
let page_start = segment.vaddr & !0xFFF; let perms =
let offset_in_page = segment.vaddr & 0xFFF; segment_perms(segment.flags).unwrap_or_else(|| crate::halt(uart, "W+X segment"));
let total_size = offset_in_page + segment.mem_size; let page_start = segment.vaddr & !(PAGE_SIZE - 1);
let seg_pages = (total_size + 4095) / 4096; let offset_in_page = segment.vaddr - page_start;
let pages = offset_in_page
.checked_add(segment.mem_size)
.map(|total| total.div_ceil(PAGE_SIZE))
.unwrap_or_else(|| crate::halt(uart, "segment too large"));
if pages == 0 {
continue;
}
for p in 0..seg_pages { kern()
let page = mmu::allocator::alloc_zeroed_page(); .map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms)
let page_va = page_start + p * 4096; .unwrap_or_else(|_| crate::halt(uart, "cannot map segment"));
let seg_file_start = if p == 0 { 0 } else { p * 4096 - offset_in_page };
let dest_off = if p == 0 { offset_in_page } else { 0 };
let copy_len = if seg_file_start < segment.file_size {
(segment.file_size - seg_file_start).min(4096 - dest_off)
} else { 0 };
if copy_len > 0 { let copy_len = segment.file_size.min(segment.mem_size);
let src = segment.file_offset + seg_file_start; let data = segment
if src + copy_len <= file_size { .file_offset
unsafe { .checked_add(copy_len)
core::ptr::copy_nonoverlapping( .and_then(|end| file.get(segment.file_offset..end))
buf.as_ptr().add(src), .unwrap_or_else(|| crate::halt(uart, "segment outside file"));
(page as *mut u8).add(dest_off), if !copy_to_user(pid, segment.vaddr, data) {
copy_len, crate::halt(uart, "segment copy failed");
);
}
}
}
let flags = if segment.flags.execute {
mmu::Sv39Flags::user_rx()
} else if segment.flags.write {
mmu::Sv39Flags::user_rw()
} else {
mmu::Sv39Flags::user_rx()
};
mmu::map(user_pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(page_va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags,
});
} }
} }
for i in 0..16usize { let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE;
let sp = mmu::allocator::alloc_zeroed_page(); kern()
mmu::map(user_pt, mmu::Mapping { .map_fresh_range(
va: types::address::VirtualAddress::from_raw(USER_VA_BASE - (16 - i) * 4096), pid,
pa: types::address::PhysicalAddress::from_raw(sp), VirtualAddress::from_raw(stack_bottom),
pages: 1, USER_STACK_PAGES,
flags: mmu::Sv39Flags::user_rw(), PagePerms::ReadWrite,
}); )
} .unwrap_or_else(|_| crate::halt(uart, "cannot map stack"));
kern().platform_mut().set_page_table(pid, user_pt as *const mmu::PageTable as usize);
elf_info.entry_point elf_info.entry_point
} }

View File

@@ -1,11 +1,9 @@
use crate::hw::virtio_blk::VirtioBlk; use crate::hw::virtio_blk::VirtioBlk;
const SECTOR_SIZE: usize = 512; const SECTOR_SIZE: usize = 512;
const MAX_FILE_SIZE: usize = 64 * 1024; // 64K max for init binary
struct BootFat16 { struct BootFat16 {
reserved_sectors: u16, reserved_sectors: u16,
sectors_per_fat: u16,
root_dir_start: u32, root_dir_start: u32,
root_dir_sectors: u32, root_dir_sectors: u32,
data_start: u32, data_start: u32,
@@ -19,7 +17,9 @@ impl BootFat16 {
} }
let bytes_per_sector = u16::from_le_bytes([boot_sector[11], boot_sector[12]]); let bytes_per_sector = u16::from_le_bytes([boot_sector[11], boot_sector[12]]);
if bytes_per_sector != SECTOR_SIZE as u16 { return None; } if bytes_per_sector != SECTOR_SIZE as u16 {
return None;
}
let sectors_per_cluster = boot_sector[13]; let sectors_per_cluster = boot_sector[13];
let reserved_sectors = u16::from_le_bytes([boot_sector[14], boot_sector[15]]); let reserved_sectors = u16::from_le_bytes([boot_sector[14], boot_sector[15]]);
@@ -29,12 +29,11 @@ impl BootFat16 {
let fat_start = reserved_sectors as u32; let fat_start = reserved_sectors as u32;
let root_dir_start = fat_start + (num_fats as u32) * (sectors_per_fat as u32); let root_dir_start = fat_start + (num_fats as u32) * (sectors_per_fat as u32);
let root_dir_sectors = ((root_entry_count as u32) * 32 + 511) / 512; let root_dir_sectors = ((root_entry_count as u32) * 32).div_ceil(512);
let data_start = root_dir_start + root_dir_sectors; let data_start = root_dir_start + root_dir_sectors;
Some(Self { Some(Self {
reserved_sectors, reserved_sectors,
sectors_per_fat,
root_dir_start, root_dir_start,
root_dir_sectors, root_dir_sectors,
data_start, data_start,
@@ -50,7 +49,9 @@ impl BootFat16 {
fn to_fat_name(name: &[u8]) -> [u8; 11] { fn to_fat_name(name: &[u8]) -> [u8; 11] {
let mut fat = [b' '; 11]; let mut fat = [b' '; 11];
let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len()); 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(); } for i in 0..dot_pos.min(8) {
fat[i] = name[i].to_ascii_uppercase();
}
if dot_pos < name.len() { if dot_pos < name.len() {
for i in 0..(name.len() - dot_pos - 1).min(3) { for i in 0..(name.len() - dot_pos - 1).min(3) {
fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase(); fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase();
@@ -67,7 +68,9 @@ pub fn read_file_from_disk(
let mut sector_buf = [0u8; SECTOR_SIZE]; let mut sector_buf = [0u8; SECTOR_SIZE];
// Read boot sector // Read boot sector
if !blk.read_sector(0, &mut sector_buf) { return None; } if !blk.read_sector(0, &mut sector_buf) {
return None;
}
let fs = BootFat16::parse(&sector_buf)?; let fs = BootFat16::parse(&sector_buf)?;
let fat_name = to_fat_name(filename); let fat_name = to_fat_name(filename);
@@ -78,36 +81,52 @@ pub fn read_file_from_disk(
let mut found = false; let mut found = false;
for s in 0..fs.root_dir_sectors { for s in 0..fs.root_dir_sectors {
if !blk.read_sector((fs.root_dir_start + s) as u32, &mut sector_buf) { continue; } if !blk.read_sector(fs.root_dir_start + s, &mut sector_buf) {
continue;
}
for i in 0..(SECTOR_SIZE / 32) { for i in 0..(SECTOR_SIZE / 32) {
let off = i * 32; let off = i * 32;
if sector_buf[off] == 0x00 { break; } if sector_buf[off] == 0x00 {
if sector_buf[off] == 0xE5 { continue; } break;
}
if sector_buf[off] == 0xE5 {
continue;
}
if sector_buf[off..off + 11] == fat_name { if sector_buf[off..off + 11] == fat_name {
first_cluster = u16::from_le_bytes([sector_buf[off + 26], sector_buf[off + 27]]); first_cluster = u16::from_le_bytes([sector_buf[off + 26], sector_buf[off + 27]]);
file_size = u32::from_le_bytes([ file_size = u32::from_le_bytes([
sector_buf[off + 28], sector_buf[off + 29], sector_buf[off + 28],
sector_buf[off + 30], sector_buf[off + 31], sector_buf[off + 29],
sector_buf[off + 30],
sector_buf[off + 31],
]); ]);
found = true; found = true;
break; break;
} }
} }
if found { break; } if found {
break;
}
} }
if !found { return None; } if !found {
return None;
}
let size = (file_size as usize).min(out_buf.len()); let size = (file_size as usize).min(out_buf.len());
let mut read = 0; let mut read = 0;
let mut cluster = first_cluster; let mut cluster = first_cluster;
let sectors_per_cluster = fs.sectors_per_cluster as u32; let sectors_per_cluster = fs.sectors_per_cluster as u32;
while read < size && cluster >= 2 && cluster < 0xFFF8 { while read < size && (2..0xFFF8).contains(&cluster) {
for s in 0..sectors_per_cluster { for s in 0..sectors_per_cluster {
if read >= size { break; } if read >= size {
break;
}
let sector = fs.cluster_to_sector(cluster) + s; let sector = fs.cluster_to_sector(cluster) + s;
if !blk.read_sector(sector, &mut sector_buf) { return Some(read); } if !blk.read_sector(sector, &mut sector_buf) {
return Some(read);
}
let chunk = (size - read).min(SECTOR_SIZE); let chunk = (size - read).min(SECTOR_SIZE);
out_buf[read..read + chunk].copy_from_slice(&sector_buf[..chunk]); out_buf[read..read + chunk].copy_from_slice(&sector_buf[..chunk]);
read += chunk; read += chunk;
@@ -117,7 +136,9 @@ pub fn read_file_from_disk(
let fat_offset = (cluster as u32) * 2; let fat_offset = (cluster as u32) * 2;
let fat_sector = fs.reserved_sectors as u32 + fat_offset / SECTOR_SIZE as u32; let fat_sector = fs.reserved_sectors as u32 + fat_offset / SECTOR_SIZE as u32;
let fat_byte = (fat_offset % SECTOR_SIZE as u32) as usize; let fat_byte = (fat_offset % SECTOR_SIZE as u32) as usize;
if !blk.read_sector(fat_sector, &mut sector_buf) { break; } if !blk.read_sector(fat_sector, &mut sector_buf) {
break;
}
cluster = u16::from_le_bytes([sector_buf[fat_byte], sector_buf[fat_byte + 1]]); cluster = u16::from_le_bytes([sector_buf[fat_byte], sector_buf[fat_byte + 1]]);
} }

View File

@@ -1,13 +1,9 @@
use super::{dma_buffer, info_page, kern, map_mmio};
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::hw::virtio_gpu::VirtioGpu; use crate::hw::virtio_gpu::VirtioGpu;
use crate::mmu;
use types::display::Dimension; use types::display::Dimension;
use types::id::ProcessId; use types::id::ProcessId;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
const GPU_VA: usize = 0x20000; const GPU_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000; const VQUEUE_VA: usize = 0x21000;
const CMDBUF_VA: usize = 0x23000; const CMDBUF_VA: usize = 0x23000;
@@ -15,6 +11,8 @@ const PHYSINFO_VA: usize = 0x24000;
const FB_VA: usize = 0x100000; const FB_VA: usize = 0x100000;
const DEFAULT_WIDTH: Dimension = Dimension::new(640); const DEFAULT_WIDTH: Dimension = Dimension::new(640);
const DEFAULT_HEIGHT: Dimension = Dimension::new(480); const DEFAULT_HEIGHT: Dimension = Dimension::new(480);
/// The framebuffer alone is ~1000 pages at 1280x800.
const DISPLAY_PAGE_LIMIT: usize = 2048;
pub fn configure(uart: &Uart, display_pid: ProcessId) { pub fn configure(uart: &Uart, display_pid: ProcessId) {
let gpu = match VirtioGpu::probe() { let gpu = match VirtioGpu::probe() {
@@ -25,28 +23,24 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
} }
}; };
let pt_addr = kern().platform_mut().page_table_for(display_pid); kern()
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) }; .set_page_limit(display_pid, DISPLAY_PAGE_LIMIT)
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
map_page(pt, GPU_VA, gpu.base()); map_mmio(uart, display_pid, GPU_VA, gpu.base(), 1);
let vq_phys = dma_buffer(uart, display_pid, VQUEUE_VA, 2);
let vq_phys = alloc_contiguous(2); let cmd_phys = dma_buffer(uart, display_pid, CMDBUF_VA, 1);
map_range(pt, VQUEUE_VA, vq_phys, 2);
let cmd_phys = mmu::allocator::alloc_zeroed_page();
map_page(pt, CMDBUF_VA, cmd_phys);
gpu.init_device(vq_phys); gpu.init_device(vq_phys);
let queue = hw_virtio::queue::VirtioQueue::new( let queue = hw_virtio::queue::VirtioQueue::new(hw_virtio::queue::QueueAddrs::from_base(
hw_virtio::queue::QueueAddrs::from_base(vq_phys), crate::mmu::phys_to_virt(vq_phys),
); ));
let dma = hw_virtio::gpu::GpuDmaAddrs { let dma = hw_virtio::gpu::GpuDmaAddrs {
cmd_pa: types::address::PhysicalAddress::from_raw(cmd_phys), cmd_pa: types::address::PhysicalAddress::from_raw(cmd_phys),
cmd_va: types::address::VirtualAddress::from_raw(cmd_phys), cmd_va: types::address::VirtualAddress::from_raw(crate::mmu::phys_to_virt(cmd_phys)),
resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512), resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512),
}; };
use hw_virtio::mmio::MmioAccess;
let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) { let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) {
Some((w, h)) => { Some((w, h)) => {
@@ -63,20 +57,21 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
} }
}; };
let fb_pages = (width.raw() as usize * height.raw() as usize * 4 + 4095) / 4096; let fb_pages = (width.raw() as usize * height.raw() as usize * 4).div_ceil(4096);
let fb_phys = alloc_contiguous(fb_pages); let fb_phys = dma_buffer(uart, display_pid, FB_VA, fb_pages);
map_range(pt, FB_VA, fb_phys, fb_pages);
let info_phys = mmu::allocator::alloc_zeroed_page(); info_page(
map_page(pt, PHYSINFO_VA, info_phys); uart,
unsafe { display_pid,
let info = info_phys as *mut u64; PHYSINFO_VA,
core::ptr::write_volatile(info, cmd_phys as u64); &[
core::ptr::write_volatile(info.add(1), (cmd_phys + 512) as u64); cmd_phys as u64,
core::ptr::write_volatile(info.add(2), fb_phys as u64); (cmd_phys + 512) as u64,
core::ptr::write_volatile(info.add(3), width.raw() as u64); fb_phys as u64,
core::ptr::write_volatile(info.add(4), height.raw() as u64); width.raw() as u64,
} height.raw() as u64,
],
);
if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) { if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) {
uart.write_str("k-os: VirtIO GPU configured\n"); uart.write_str("k-os: VirtIO GPU configured\n");
@@ -84,26 +79,3 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
uart.write_str("k-os: VirtIO GPU setup failed (display will be headless)\n"); uart.write_str("k-os: VirtIO GPU setup failed (display will be headless)\n");
} }
} }
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(pa),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
for i in 0..count {
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
}
}
fn alloc_contiguous(pages: usize) -> usize {
let first = mmu::allocator::alloc_zeroed_page();
for _ in 1..pages {
mmu::allocator::alloc_zeroed_page();
}
first
}

View File

@@ -1,5 +1,6 @@
use super::{dma_buffer, info_page, map_mmio};
use crate::board;
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::{board, mmu};
use types::id::ProcessId; use types::id::ProcessId;
const UART_VA: usize = 0x20000; const UART_VA: usize = 0x20000;
@@ -8,34 +9,23 @@ const INPUT_VQUEUE_VA: usize = 0x26000;
const INPUT_EVBUF_VA: usize = 0x28000; const INPUT_EVBUF_VA: usize = 0x28000;
const INPUT_PHYSINFO_VA: usize = 0x29000; const INPUT_PHYSINFO_VA: usize = 0x29000;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
pub fn configure(uart: &Uart, input_pid: ProcessId) { pub fn configure(uart: &Uart, input_pid: ProcessId) {
let pt_addr = kern().platform_mut().page_table_for(input_pid);
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
// Always map UART for fallback // Always map UART for fallback
map_page(pt, UART_VA, board::UART_BASE); map_mmio(uart, input_pid, UART_VA, board::UART_BASE, 1);
// Try to find VirtIO input device // Try to find VirtIO input device
if let Some(mmio) = hw_virtio::mmio::scan_for_device(hw_virtio::mmio::DEVICE_ID_INPUT) { let first = crate::mmu::phys_to_virt(hw_virtio::mmio::MMIO_FIRST);
let base = mmio.base(); if let Some(mmio) = hw_virtio::mmio::scan_for_device(first, hw_virtio::mmio::DEVICE_ID_INPUT) {
map_page(pt, INPUT_MMIO_VA, base); let base = crate::mmu::virt_to_phys(mmio.base());
map_mmio(uart, input_pid, INPUT_MMIO_VA, base, 1);
let vq_phys = alloc_contiguous(2); let vq_phys = dma_buffer(uart, input_pid, INPUT_VQUEUE_VA, 2);
map_range(pt, INPUT_VQUEUE_VA, vq_phys, 2); let evbuf_phys = dma_buffer(uart, input_pid, INPUT_EVBUF_VA, 1);
info_page(
let evbuf_phys = mmu::allocator::alloc_zeroed_page(); uart,
map_page(pt, INPUT_EVBUF_VA, evbuf_phys); input_pid,
INPUT_PHYSINFO_VA,
let info_phys = mmu::allocator::alloc_zeroed_page(); &[evbuf_phys as u64, vq_phys as u64],
map_page(pt, INPUT_PHYSINFO_VA, info_phys); );
unsafe {
core::ptr::write_volatile(info_phys as *mut u64, evbuf_phys as u64);
core::ptr::write_volatile((info_phys as *mut u64).add(1), vq_phys as u64);
}
hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32); hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32);
uart.write_str("k-os: VirtIO keyboard configured\n"); uart.write_str("k-os: VirtIO keyboard configured\n");
@@ -43,26 +33,3 @@ pub fn configure(uart: &Uart, input_pid: ProcessId) {
uart.write_str("k-os: UART mapped for input\n"); uart.write_str("k-os: UART mapped for input\n");
} }
} }
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(pa),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
for i in 0..count {
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
}
}
fn alloc_contiguous(pages: usize) -> usize {
let first = mmu::allocator::alloc_zeroed_page();
for _ in 1..pages {
mmu::allocator::alloc_zeroed_page();
}
first
}

View File

@@ -1,3 +1,91 @@
pub mod gpu_setup; pub mod gpu_setup;
pub mod input_setup; pub mod input_setup;
pub mod storage_setup; pub mod storage_setup;
use crate::hw::uart::Uart;
use types::address::{PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
use types::id::ProcessId;
use types::mapping::PagePerms;
// Boot-time device wiring. Everything here goes through the kernel's
// mapping API so the kernel knows about every page a driver can touch.
// Device regions come from the board table and belong to init, which
// also holds capabilities to them; moving drivers onto those caps is
// userspace work.
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
fn pages(uart: &Uart, n: usize) -> PageCount {
u16::try_from(n)
.ok()
.and_then(PageCount::new)
.unwrap_or_else(|| crate::halt(uart, "bad page count"))
}
/// Maps the board device region at `pa` into `pid` at `va`. The region
/// belongs to init (see `kernel::Kernel::register_board`); `n` must match
/// its size.
pub(crate) fn map_mmio(uart: &Uart, pid: ProcessId, va: usize, pa: usize, n: usize) {
let id = kern()
.device_region_at(PhysicalAddress::from_raw(pa))
.unwrap_or_else(|| crate::halt(uart, "no such board device"));
if kern().region_pages(id).map(|p| p.value() as usize) != Some(n) {
crate::halt(uart, "device size mismatch");
}
kern()
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
.unwrap_or_else(|_| crate::halt(uart, "device map failed"));
}
/// Physically contiguous, zeroed DMA memory for `owner`, mapped read-write
/// at `va`. Returns its physical base for programming the device.
pub(crate) fn dma_buffer(uart: &Uart, owner: ProcessId, va: usize, n: usize) -> usize {
let id = kern()
.create_pool_region(owner, pages(uart, n))
.unwrap_or_else(|_| crate::halt(uart, "DMA region failed"));
kern()
.map_region(
id,
owner,
VirtualAddress::from_raw(va),
PagePerms::ReadWrite,
)
.unwrap_or_else(|_| crate::halt(uart, "DMA map failed"));
kern()
.region_base(id)
.unwrap_or_else(|| crate::halt(uart, "DMA region vanished"))
.raw()
}
/// Shares a fresh region between `owner` and `other` at the same `va`.
pub(crate) fn shared_buffer(uart: &Uart, owner: ProcessId, other: ProcessId, va: usize, n: usize) {
let id = kern()
.create_pool_region(owner, pages(uart, n))
.unwrap_or_else(|_| crate::halt(uart, "shared region failed"));
for pid in [owner, other] {
kern()
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
.unwrap_or_else(|_| crate::halt(uart, "shared map failed"));
}
}
/// Writes `words` into a fresh page mapped at `va` in `pid`, so a driver
/// can learn the physical addresses of its DMA buffers.
pub(crate) fn info_page(uart: &Uart, pid: ProcessId, va: usize, words: &[u64]) {
let va = VirtualAddress::from_raw(va);
kern()
.map_fresh_range(pid, va, 1, PagePerms::ReadOnly)
.unwrap_or_else(|_| crate::halt(uart, "info page failed"));
let pa = kern()
.user_page_frame(pid, va)
.unwrap_or_else(|| crate::halt(uart, "info page vanished"));
let info = crate::mmu::phys_to_virt(pa.raw()) as *mut u64;
for (i, &w) in words.iter().enumerate() {
// SAFETY: `pa` is a pool frame just mapped for `pid`, reachable at
// `phys_to_virt`, and `words` fits in one page.
unsafe { core::ptr::write_volatile(info.add(i), w) };
}
}

View File

@@ -1,11 +1,7 @@
use super::{dma_buffer, info_page, map_mmio, shared_buffer};
use crate::hw::uart::Uart; use crate::hw::uart::Uart;
use crate::mmu;
use types::id::ProcessId; use types::id::ProcessId;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
const VIRTIO_VA: usize = 0x20000; const VIRTIO_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000; const VQUEUE_VA: usize = 0x21000;
const REQHDR_VA: usize = 0x23000; const REQHDR_VA: usize = 0x23000;
@@ -13,102 +9,28 @@ const PHYSINFO_VA: usize = 0x24000;
const SHARED_BUF_VA: usize = 0x30000; const SHARED_BUF_VA: usize = 0x30000;
const SHARED_BUF_PAGES: usize = 16; const SHARED_BUF_PAGES: usize = 16;
pub fn configure( pub fn configure(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId, virtio_base: usize) {
uart: &Uart, map_mmio(uart, storage_pid, VIRTIO_VA, virtio_base, 1);
init_pid: ProcessId, let vqueue_phys = dma_buffer(uart, storage_pid, VQUEUE_VA, 2);
storage_pid: ProcessId, let reqhdr_phys = dma_buffer(uart, storage_pid, REQHDR_VA, 1);
virtio_base: usize, info_page(
) { uart,
let storage_pt = get_pt(storage_pid); storage_pid,
let init_pt = get_pt(init_pid); PHYSINFO_VA,
&[
map_virtio_mmio(storage_pt, virtio_base); reqhdr_phys as u64,
let vqueue_phys = alloc_and_map_virtqueue(storage_pt); (reqhdr_phys + 16) as u64,
let reqhdr_phys = alloc_and_map_reqhdr(storage_pt); (reqhdr_phys + 32) as u64,
write_phys_info(storage_pt, reqhdr_phys); ],
);
reinit_device(virtio_base, vqueue_phys); reinit_device(virtio_base, vqueue_phys);
uart.write_str("k-os: VirtIO configured for storage\n"); uart.write_str("k-os: VirtIO configured for storage\n");
map_shared_buffer(init_pt, storage_pt); shared_buffer(uart, init_pid, storage_pid, SHARED_BUF_VA, SHARED_BUF_PAGES);
uart.write_str("k-os: shared buffer mapped\n"); uart.write_str("k-os: shared buffer mapped\n");
} }
fn get_pt(pid: ProcessId) -> &'static mut mmu::PageTable {
let addr = kern().platform_mut().page_table_for(pid);
unsafe { &mut *(addr as *mut mmu::PageTable) }
}
fn map_virtio_mmio(pt: &mut mmu::PageTable, virtio_base: usize) {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(VIRTIO_VA),
pa: types::address::PhysicalAddress::from_raw(virtio_base),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
fn alloc_and_map_virtqueue(pt: &mut mmu::PageTable) -> usize {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::allocator::alloc_zeroed_page();
for i in 0..2usize {
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(VQUEUE_VA + i * 4096),
pa: types::address::PhysicalAddress::from_raw(phys + i * 4096),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
phys
}
fn alloc_and_map_reqhdr(pt: &mut mmu::PageTable) -> usize {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(REQHDR_VA),
pa: types::address::PhysicalAddress::from_raw(phys),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
phys
}
fn write_phys_info(pt: &mut mmu::PageTable, reqhdr_phys: usize) {
let phys = mmu::allocator::alloc_zeroed_page();
mmu::map(pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(PHYSINFO_VA),
pa: types::address::PhysicalAddress::from_raw(phys),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
unsafe {
let info = phys as *mut u64;
core::ptr::write_volatile(info, reqhdr_phys as u64);
core::ptr::write_volatile(info.add(1), (reqhdr_phys + 16) as u64);
core::ptr::write_volatile(info.add(2), (reqhdr_phys + 32) as u64);
}
}
fn reinit_device(base: usize, vqueue_phys: usize) { fn reinit_device(base: usize, vqueue_phys: usize) {
let mmio = hw_virtio::mmio::DirectMmio::new(base); let mmio = hw_virtio::mmio::DirectMmio::new(crate::mmu::phys_to_virt(base));
hw_virtio::mmio::init_legacy_device(&mmio, vqueue_phys, hw_virtio::queue::QUEUE_SIZE as u32); hw_virtio::mmio::init_legacy_device(&mmio, vqueue_phys, hw_virtio::queue::QUEUE_SIZE as u32);
} }
fn map_shared_buffer(init_pt: &mut mmu::PageTable, storage_pt: &mut mmu::PageTable) {
for i in 0..SHARED_BUF_PAGES {
let page = mmu::allocator::alloc_zeroed_page();
let va = SHARED_BUF_VA + i * 4096;
let mapping = mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
};
mmu::map(init_pt, mapping);
mmu::map(storage_pt, mmu::Mapping {
va: types::address::VirtualAddress::from_raw(va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
});
}
}

View File

@@ -1,21 +1,37 @@
use crate::board; use crate::board;
const PLIC_PRIORITY: usize = board::PLIC_BASE; const PLIC: usize = crate::mmu::phys_to_virt(board::PLIC_BASE);
const PLIC_S_ENABLE: usize = board::PLIC_BASE + 0x2080; const PLIC_PRIORITY: usize = PLIC;
const PLIC_S_THRESHOLD: usize = board::PLIC_BASE + 0x201000;
const PLIC_S_CLAIM: usize = board::PLIC_BASE + 0x201004; // External interrupts go to the boot hart only: its supervisor context is
// 2 * hart + 1 on QEMU virt (even contexts are machine mode).
fn context() -> usize {
2 * crate::smp::boot_hart() + 1
}
fn enable_base() -> usize {
PLIC + 0x2000 + context() * 0x80
}
fn threshold() -> usize {
PLIC + 0x20_0000 + context() * 0x1000
}
fn claim_reg() -> usize {
threshold() + 4
}
pub struct Plic; pub struct Plic;
impl Plic { impl Plic {
pub fn init() { pub fn init() {
unsafe { core::ptr::write_volatile(PLIC_S_THRESHOLD as *mut u32, 0) }; unsafe { core::ptr::write_volatile(threshold() as *mut u32, 0) };
} }
pub fn enable(irq: u32) { pub fn enable(irq: u32) {
let word = (irq / 32) as usize; let word = (irq / 32) as usize;
let bit = irq % 32; let bit = irq % 32;
let addr = PLIC_S_ENABLE + word * 4; let addr = enable_base() + word * 4;
unsafe { unsafe {
let current = core::ptr::read_volatile(addr as *const u32); let current = core::ptr::read_volatile(addr as *const u32);
core::ptr::write_volatile(addr as *mut u32, current | (1 << bit)); core::ptr::write_volatile(addr as *mut u32, current | (1 << bit));
@@ -27,7 +43,7 @@ impl Plic {
pub fn disable(irq: u32) { pub fn disable(irq: u32) {
let word = (irq / 32) as usize; let word = (irq / 32) as usize;
let bit = irq % 32; let bit = irq % 32;
let addr = PLIC_S_ENABLE + word * 4; let addr = enable_base() + word * 4;
unsafe { unsafe {
let current = core::ptr::read_volatile(addr as *const u32); let current = core::ptr::read_volatile(addr as *const u32);
core::ptr::write_volatile(addr as *mut u32, current & !(1 << bit)); core::ptr::write_volatile(addr as *mut u32, current & !(1 << bit));
@@ -35,10 +51,10 @@ impl Plic {
} }
pub fn claim() -> u32 { pub fn claim() -> u32 {
unsafe { core::ptr::read_volatile(PLIC_S_CLAIM as *const u32) } unsafe { core::ptr::read_volatile(claim_reg() as *const u32) }
} }
pub fn complete(irq: u32) { pub fn complete(irq: u32) {
unsafe { core::ptr::write_volatile(PLIC_S_CLAIM as *mut u32, irq) }; unsafe { core::ptr::write_volatile(claim_reg() as *mut u32, irq) };
} }
} }

View File

@@ -7,10 +7,13 @@ pub fn read_time() -> u64 {
} }
fn sbi_set_timer(stime: u64) { fn sbi_set_timer(stime: u64) {
// SBI returns an error code in a0 and a value in a1; both are
// clobbered even though we ignore them.
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"ecall", "ecall",
in("a0") stime, inlateout("a0") stime => _,
lateout("a1") _,
in("a6") 0usize, in("a6") 0usize,
in("a7") 0x54494D45usize, in("a7") 0x54494D45usize,
); );

View File

@@ -6,7 +6,9 @@ pub struct Uart {
impl Uart { impl Uart {
pub fn new() -> Self { pub fn new() -> Self {
Self { base: board::UART_BASE } Self {
base: crate::mmu::phys_to_virt(board::UART_BASE),
}
} }
pub fn put_char(&self, byte: u8) { pub fn put_char(&self, byte: u8) {
@@ -30,7 +32,11 @@ impl Uart {
let mut i = 0; let mut i = 0;
while value > 0 { while value > 0 {
let digit = (value & 0xF) as u8; let digit = (value & 0xF) as u8;
buf[i] = if digit < 10 { b'0' + digit } else { b'a' + digit - 10 }; buf[i] = if digit < 10 {
b'0' + digit
} else {
b'a' + digit - 10
};
value >>= 4; value >>= 4;
i += 1; i += 1;
} }

View File

@@ -1,6 +1,7 @@
use hw_virtio::mmio::{self, DirectMmio}; use crate::mmu::{phys_to_virt, virt_to_phys};
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
use hw_virtio::block::{self, DmaAddrs}; use hw_virtio::block::{self, DmaAddrs};
use hw_virtio::mmio::{self, DirectMmio};
use hw_virtio::queue::{QUEUE_SIZE, QueueAddrs, VirtioQueue};
use types::address::{PhysicalAddress, VirtualAddress}; use types::address::{PhysicalAddress, VirtualAddress};
const SECTOR_SIZE: usize = 512; const SECTOR_SIZE: usize = 512;
@@ -11,7 +12,11 @@ struct VirtqMem {
} }
static mut VQUEUE: VirtqMem = VirtqMem { _data: [0; 8192] }; static mut VQUEUE: VirtqMem = VirtqMem { _data: [0; 8192] };
static mut REQ_HEADER: block::BlockReqHeader = block::BlockReqHeader { req_type: 0, reserved: 0, sector: 0 }; static mut REQ_HEADER: block::BlockReqHeader = block::BlockReqHeader {
req_type: 0,
reserved: 0,
sector: 0,
};
static mut REQ_STATUS: u8 = 0xFF; static mut REQ_STATUS: u8 = 0xFF;
static mut SECTOR_BUF: [u8; SECTOR_SIZE] = [0; SECTOR_SIZE]; static mut SECTOR_BUF: [u8; SECTOR_SIZE] = [0; SECTOR_SIZE];
@@ -23,29 +28,38 @@ pub struct VirtioBlk {
impl VirtioBlk { impl VirtioBlk {
pub fn probe() -> Option<Self> { pub fn probe() -> Option<Self> {
let mmio = mmio::scan_for_device(mmio::DEVICE_ID_BLOCK)?; let mmio = mmio::scan_for_device(phys_to_virt(mmio::MMIO_FIRST), mmio::DEVICE_ID_BLOCK)?;
let vq_phys = unsafe { core::ptr::addr_of!(VQUEUE) as usize }; // The device sees physical addresses; the CPU uses kernel ones.
mmio::init_legacy_device(&mmio, vq_phys, QUEUE_SIZE as u32); let vq_va = core::ptr::addr_of!(VQUEUE) as usize;
mmio::init_legacy_device(&mmio, virt_to_phys(vq_va), QUEUE_SIZE as u32);
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys)); let queue = VirtioQueue::new(QueueAddrs::from_base(vq_va));
let dma = unsafe { let pair = |va: usize| {
DmaAddrs { (
header_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_HEADER) as usize), PhysicalAddress::from_raw(virt_to_phys(va)),
header_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_HEADER) as usize), VirtualAddress::from_raw(va),
status_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_STATUS) as usize), )
status_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_STATUS) as usize), };
data_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(SECTOR_BUF) as usize), let (header_pa, header_va) = pair(core::ptr::addr_of_mut!(REQ_HEADER) as usize);
data_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(SECTOR_BUF) as usize), let (status_pa, status_va) = pair(core::ptr::addr_of_mut!(REQ_STATUS) as usize);
} let (data_pa, data_va) = pair(core::ptr::addr_of_mut!(SECTOR_BUF) as usize);
let dma = DmaAddrs {
header_pa,
header_va,
status_pa,
status_va,
data_pa,
data_va,
}; };
Some(Self { mmio, queue, dma }) Some(Self { mmio, queue, dma })
} }
/// Physical address of the device's MMIO window.
pub fn base(&self) -> usize { pub fn base(&self) -> usize {
self.mmio.base() virt_to_phys(self.mmio.base())
} }
pub fn read_sector(&mut self, sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> bool { pub fn read_sector(&mut self, sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> bool {

View File

@@ -1,6 +1,7 @@
use hw_virtio::mmio::{self, DirectMmio}; use crate::mmu::{phys_to_virt, virt_to_phys};
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
use hw_virtio::gpu::{self, GpuDmaAddrs}; use hw_virtio::gpu::{self, GpuDmaAddrs};
use hw_virtio::mmio::{self, DirectMmio};
use hw_virtio::queue::{QUEUE_SIZE, QueueAddrs, VirtioQueue};
use types::address::{PhysicalAddress, VirtualAddress}; use types::address::{PhysicalAddress, VirtualAddress};
use types::display::Dimension; use types::display::Dimension;
@@ -10,12 +11,13 @@ pub struct VirtioGpu {
impl VirtioGpu { impl VirtioGpu {
pub fn probe() -> Option<Self> { pub fn probe() -> Option<Self> {
let mmio = mmio::scan_for_device(mmio::DEVICE_ID_GPU)?; let mmio = mmio::scan_for_device(phys_to_virt(mmio::MMIO_FIRST), mmio::DEVICE_ID_GPU)?;
Some(Self { mmio }) Some(Self { mmio })
} }
/// Physical address of the device's MMIO window.
pub fn base(&self) -> usize { pub fn base(&self) -> usize {
self.mmio.base() virt_to_phys(self.mmio.base())
} }
pub fn mmio(&self) -> &DirectMmio { pub fn mmio(&self) -> &DirectMmio {
@@ -26,15 +28,29 @@ impl VirtioGpu {
mmio::init_legacy_device(&self.mmio, vqueue_phys, QUEUE_SIZE as u32); mmio::init_legacy_device(&self.mmio, vqueue_phys, QUEUE_SIZE as u32);
} }
pub fn setup_framebuffer(&self, cmd_phys: usize, vq_phys: usize, fb_phys: usize, width: Dimension, height: Dimension) -> bool { pub fn setup_framebuffer(
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys)); &self,
cmd_phys: usize,
vq_phys: usize,
fb_phys: usize,
width: Dimension,
height: Dimension,
) -> bool {
let queue = VirtioQueue::new(QueueAddrs::from_base(phys_to_virt(vq_phys)));
let dma = GpuDmaAddrs { let dma = GpuDmaAddrs {
cmd_pa: PhysicalAddress::from_raw(cmd_phys), cmd_pa: PhysicalAddress::from_raw(cmd_phys),
cmd_va: VirtualAddress::from_raw(cmd_phys), cmd_va: VirtualAddress::from_raw(phys_to_virt(cmd_phys)),
resp_pa: PhysicalAddress::from_raw(cmd_phys + 512), resp_pa: PhysicalAddress::from_raw(cmd_phys + 512),
}; };
let ok1 = gpu::create_2d(&self.mmio, &queue, &dma, 1, width, height); let ok1 = gpu::create_2d(&self.mmio, &queue, &dma, 1, width, height);
let ok2 = gpu::attach_backing(&self.mmio, &queue, &dma, 1, PhysicalAddress::from_raw(fb_phys), width.raw() as u32 * height.raw() as u32 * 4); let ok2 = gpu::attach_backing(
&self.mmio,
&queue,
&dma,
1,
PhysicalAddress::from_raw(fb_phys),
width.raw() as u32 * height.raw() as u32 * 4,
);
let ok3 = gpu::set_scanout(&self.mmio, &queue, &dma, 0, 1, width, height); let ok3 = gpu::set_scanout(&self.mmio, &queue, &dma, 0, 1, width, height);
ok1 && ok2 && ok3 ok1 && ok2 && ok3
} }

View File

@@ -18,8 +18,13 @@ impl<T> KernelCell<T> {
} }
} }
pub fn init(&self, value: T) { /// Initializes the cell by letting `build` write the value straight
unsafe { (*self.inner.get()).write(value) }; /// into its storage, so a large `T` never lives on the stack.
///
/// # Safety
/// `build` must fully initialize the `T` behind the pointer.
pub unsafe fn init_with(&self, build: impl FnOnce(*mut T)) {
build(unsafe { (*self.inner.get()).as_mut_ptr() });
self.initialized.store(true, Ordering::Release); self.initialized.store(true, Ordering::Release);
} }

View File

@@ -9,6 +9,7 @@ mod hw;
mod kernel_cell; mod kernel_cell;
mod mmu; mod mmu;
mod platform; mod platform;
mod smp;
mod trap; mod trap;
use core::arch::global_asm; use core::arch::global_asm;
@@ -17,55 +18,168 @@ use kernel_cell::KernelCell;
static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit(); static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit();
fn kern_mut() -> &'static mut kernel::Kernel<platform::RiscVPlatform> {
// SAFETY: boot runs with the kernel lock held and before any other
// hart has started.
unsafe { &mut *kernel_ptr() }
}
pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> { pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> {
KERNEL.as_ptr() KERNEL.as_ptr()
} }
// Firmware enters `_start` (and SBI starts other harts at
// `_secondary_start`) at the physical load address with the MMU off and
// the hart id in a0, but the kernel is linked in the upper half. Using
// only PC-relative code, the stub turns on `boot_page_table`, which maps
// RAM twice (at its physical address, just so the next instruction still
// exists, and in the upper half) plus the MMIO window high, then jumps to
// its own high address and takes the hart's kernel stack. The kernel then
// switches every hart to the real table, which maps nothing low.
global_asm!( global_asm!(
r#" r#"
.section .text.entry .section .text.entry
.globl _start .globl _start
_start: _start:
la sp, _stack_top lla t2, start_high_address
j 1f
.globl _secondary_start
_secondary_start:
lla t2, secondary_high_address
1:
li t0, 8 # smp::MAX_HARTS
bgeu a0, t0, spin
mv s0, a0
lla t0, boot_page_table
srli t0, t0, 12
li t1, 8 << 60
or t0, t0, t1
csrw satp, t0
sfence.vma
ld t0, 0(t2)
jr t0
# sp = _stacks_start + (hart + 1) * smp::STACK_SLOT
start_high:
call hart_stack
mv a0, s0
call kmain call kmain
j spin
secondary_high:
call hart_stack
mv a0, s0
call kmain_secondary
j spin
hart_stack:
lla sp, _stacks_start
addi t0, s0, 1
li t1, 0x81000
mul t0, t0, t1
add sp, sp, t0
ret
spin: spin:
wfi wfi
j spin j spin
.pushsection .rodata
.balign 8
start_high_address:
.quad start_high
secondary_high_address:
.quad secondary_high
.popsection
# Sv39 root of 1 GiB leaves; an entry is (PA >> 12) << 10 | flags, with
# flags V|R|W|A|D = 0xc7, plus X = 0xcf.
.pushsection .data
.balign 4096
boot_page_table:
.quad 0
.quad 0
.quad 0x200000cf # [2] 0x8000_0000 -> RAM, for the jump only
.fill 253, 8, 0
.quad 0x000000c7 # [256] PHYS_OFFSET + 0 -> MMIO
.quad 0
.quad 0x200000cf # [258] PHYS_OFFSET + 0x8000_0000 -> RAM
.fill 253, 8, 0
.popsection
"# "#
); );
const SHARED_BUF_VA: usize = 0x30000; const SHARED_BUF_VA: usize = 0x30000;
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn kmain() -> ! { extern "C" fn kmain(hart: usize) -> ! {
let uart = Uart::new(); let uart = Uart::new();
uart.write_str("k-os: booted on RISC-V\n"); uart.write_str("k-os: booted on RISC-V\n");
let kernel_pt = boot::hardware::init(&uart); boot::hardware::init(&uart, hart);
let mut blk = boot::hardware::probe_block_device(&uart) // Held until the boot hart first enters userspace; harts started
.unwrap_or_else(|| halt(&uart, "no block device")); // below wait for it. Not before init: zeroing .bss resets the lock.
smp::lock();
let mut blk =
boot::hardware::probe_block_device(&uart).unwrap_or_else(|| halt(&uart, "no block device"));
let (init, _) = boot::loader::load_init(&uart, &mut blk, kernel_pt); let (init, init_thread) = boot::loader::load_init(&uart, &mut blk);
let storage = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"storage.elf", "storage"); if let Some(ctx) = kern_mut().thread_context_mut(init_thread) {
let display = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"display.elf", "display"); ctx.registers_mut()[10] = SHARED_BUF_VA;
let input = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"input.elf", "input"); }
let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage");
let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display");
let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input");
let virtio_base = blk.base(); let virtio_base = blk.base();
drop(blk);
let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table"));
kern_mut()
.register_board(init.pid, &board)
.unwrap_or_else(|_| halt(&uart, "board registration failed"));
devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base); devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base);
devices::gpu_setup::configure(&uart, display.pid); devices::gpu_setup::configure(&uart, display.pid);
devices::input_setup::configure(&uart, input.pid); devices::input_setup::configure(&uart, input.pid);
boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid); boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid);
// After grant_all, so the slots init already relies on stay put.
kern_mut()
.grant_boot_authority(init.pid, &board)
.unwrap_or_else(|_| halt(&uart, "boot authority failed"));
start_other_harts(&uart, hart);
uart.write_str("k-os: entering userspace\n"); uart.write_str("k-os: entering userspace\n");
boot::loader::enter_userspace(init.pid, init.entry, SHARED_BUF_VA); trap::enter_first();
}
fn start_other_harts(uart: &Uart, boot: usize) {
unsafe extern "C" {
fn _secondary_start();
}
// Harts start with the MMU off, so at the entry's physical address.
let entry = mmu::virt_to_phys(_secondary_start as *const () as usize);
for hart in (0..smp::num_harts()).filter(|h| *h != boot) {
if !smp::sbi_hart_start(hart, entry) {
halt(uart, "could not start a hart");
}
}
uart.write_str("k-os: ");
uart.write_dec(smp::num_harts());
uart.write_str(" harts\n");
}
/// Where every hart but the boot hart starts, on its own stack.
#[unsafe(no_mangle)]
extern "C" fn kmain_secondary(_hart: usize) -> ! {
trap::init();
hw::timer::init();
mmu::activate(mmu::page_table::table_at(smp::kernel_root()));
smp::lock();
trap::enter_first();
} }
pub fn halt(uart: &Uart, msg: &str) -> ! { pub fn halt(uart: &Uart, msg: &str) -> ! {
uart.write_str("k-os: FATAL — "); uart.write_str("k-os: FATAL — ");
uart.write_str(msg); uart.write_str(msg);
uart.put_char(b'\n'); uart.put_char(b'\n');
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }
#[panic_handler] #[panic_handler]
@@ -78,5 +192,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
uart.write_dec(location.line() as usize); uart.write_dec(location.line() as usize);
} }
uart.put_char(b'\n'); uart.put_char(b'\n');
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }

View File

@@ -1,16 +1,23 @@
use super::page_table::{PageTable, PAGE_SIZE}; use super::page_table::{PAGE_SIZE, PageTable};
use crate::board;
// Boot-time bump allocator for page tables. // Boot-time bump allocator for kernel-only memory: the kernel page table
// Only used during init before the scheduler starts. // and boot scratch buffers. User memory and user page tables come from the
// kernel's frame table instead. It must never reach the user pool.
static mut NEXT_FREE: usize = 0; static mut NEXT_FREE: usize = 0;
pub fn init_allocator(start: usize) { pub fn init_allocator(start: usize) {
unsafe { NEXT_FREE = start; } unsafe {
NEXT_FREE = start;
}
} }
pub fn alloc_zeroed_page() -> usize { pub fn alloc_zeroed_page() -> usize {
unsafe { unsafe {
let addr = NEXT_FREE; let addr = NEXT_FREE;
if addr + PAGE_SIZE > super::phys_to_virt(board::USER_POOL_BASE) {
crate::halt(&crate::hw::uart::Uart::new(), "boot allocator exhausted");
}
NEXT_FREE += PAGE_SIZE; NEXT_FREE += PAGE_SIZE;
core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE); core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE);
addr addr
@@ -18,9 +25,5 @@ pub fn alloc_zeroed_page() -> usize {
} }
pub fn alloc_page_table() -> &'static mut PageTable { pub fn alloc_page_table() -> &'static mut PageTable {
unsafe { unsafe { &mut *(alloc_zeroed_page() as *mut PageTable) }
let addr = alloc_zeroed_page();
let table = &mut *(addr as *mut PageTable);
table
}
} }

View File

@@ -2,4 +2,24 @@ pub mod allocator;
pub mod page_table; pub mod page_table;
pub use allocator::init_allocator; pub use allocator::init_allocator;
pub use page_table::{activate, init_kernel_page_table, create_user_page_table, map, translate, Sv39Flags, Mapping, PageTable, PAGE_SIZE}; pub use page_table::{
PAGE_SIZE, PageTable, Sv39Flags, USER_VA_END, USER_VA_START, activate, active_root, check_wx,
create_user_root, init_kernel_page_table, map_user_page, unmap_user_page,
};
/// The kernel lives in the upper half of the address space: all of RAM and
/// the MMIO windows appear at their physical address plus this offset (the
/// start of Sv39's upper half), and the kernel image is linked there too,
/// so one offset converts any kernel address. Must match linker.ld.
pub const PHYS_OFFSET: usize = 0xFFFF_FFC0_0000_0000;
/// Where the kernel sees physical address `pa`.
pub const fn phys_to_virt(pa: usize) -> usize {
pa + PHYS_OFFSET
}
/// The physical address behind kernel address `va` (a static, the boot
/// heap, a page table). Only valid for addresses in the direct map.
pub const fn virt_to_phys(va: usize) -> usize {
va - PHYS_OFFSET
}

View File

@@ -1,6 +1,10 @@
use crate::board;
use super::allocator::alloc_page_table; use super::allocator::alloc_page_table;
use super::{phys_to_virt, virt_to_phys};
use crate::board;
use crate::smp;
use platform::{FrameSource, MapError};
use types::address::{PhysicalAddress, VirtualAddress}; use types::address::{PhysicalAddress, VirtualAddress};
use types::mapping::PagePerms;
pub use types::mapping::Mapping; pub use types::mapping::Mapping;
pub const PAGE_SIZE: usize = 4096; pub const PAGE_SIZE: usize = 4096;
@@ -20,27 +24,30 @@ pub struct Sv39Flags {
} }
impl Sv39Flags { impl Sv39Flags {
pub fn kernel_rwx() -> Self { pub fn kernel_rx() -> Self {
Self { bits: PTE_R | PTE_W | PTE_X | PTE_G } Self {
bits: PTE_R | PTE_X | PTE_G,
}
}
pub fn kernel_ro() -> Self {
Self {
bits: PTE_R | PTE_G,
}
} }
pub fn kernel_rw() -> Self { pub fn kernel_rw() -> Self {
Self { bits: PTE_R | PTE_W | PTE_G } Self {
bits: PTE_R | PTE_W | PTE_G,
}
} }
pub fn user_rx() -> Self { pub fn user(perms: PagePerms) -> Self {
Self { bits: PTE_R | PTE_X | PTE_U } let bits = match perms {
} PagePerms::ReadOnly => PTE_R | PTE_U,
PagePerms::ReadWrite => PTE_R | PTE_W | PTE_U,
pub fn user_rw() -> Self { PagePerms::ReadExecute => PTE_R | PTE_X | PTE_U,
Self { bits: PTE_R | PTE_W | PTE_U } };
}
pub fn user(read: bool, write: bool, exec: bool) -> Self {
let mut bits = PTE_U;
if read { bits |= PTE_R; }
if write { bits |= PTE_W; }
if exec { bits |= PTE_X; }
Self { bits } Self { bits }
} }
@@ -92,81 +99,223 @@ fn vpn(va: usize, level: usize) -> usize {
(va >> (12 + 9 * level)) & 0x1FF (va >> (12 + 9 * level)) & 0x1FF
} }
pub fn translate(root: &PageTable, va: usize) -> Option<usize> {
let vpn2 = vpn(va, 2);
if !root.entries[vpn2].is_valid() { return None; }
let l1 = unsafe { &*((root.entries[vpn2].ppn() << 12) as *const PageTable) };
let vpn1 = vpn(va, 1);
if !l1.entries[vpn1].is_valid() { return None; }
let l0 = unsafe { &*((l1.entries[vpn1].ppn() << 12) as *const PageTable) };
let vpn0 = vpn(va, 0);
if !l0.entries[vpn0].is_valid() { return None; }
let pa = (l0.entries[vpn0].ppn() << 12) as usize | (va & 0xFFF);
Some(pa)
}
fn map_page(root: &mut PageTable, va: usize, pa: usize, flags: Sv39Flags) { fn map_page(root: &mut PageTable, va: usize, pa: usize, flags: Sv39Flags) {
let vpn2 = vpn(va, 2); let vpn2 = vpn(va, 2);
let vpn1 = vpn(va, 1); let vpn1 = vpn(va, 1);
let vpn0 = vpn(va, 0); let vpn0 = vpn(va, 0);
if !root.entries[vpn2].is_valid() { if !root.entries[vpn2].is_valid() {
let table = alloc_page_table(); root.entries[vpn2] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
let ppn = (table as *const PageTable as u64) >> 12;
root.entries[vpn2] = PageTableEntry::new_branch(ppn);
} }
let l1 = table_at((root.entries[vpn2].ppn() << 12) as usize);
let l1_addr = (root.entries[vpn2].ppn() << 12) as *mut PageTable;
let l1 = unsafe { &mut *l1_addr };
if !l1.entries[vpn1].is_valid() { if !l1.entries[vpn1].is_valid() {
let table = alloc_page_table(); l1.entries[vpn1] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
let ppn = (table as *const PageTable as u64) >> 12;
l1.entries[vpn1] = PageTableEntry::new_branch(ppn);
} }
let l0 = table_at((l1.entries[vpn1].ppn() << 12) as usize);
let l0_addr = (l1.entries[vpn1].ppn() << 12) as *mut PageTable;
let l0 = unsafe { &mut *l0_addr };
let ppn = (pa as u64) >> 12; let ppn = (pa as u64) >> 12;
l0.entries[vpn0] = PageTableEntry::new_leaf(ppn, flags); l0.entries[vpn0] = PageTableEntry::new_leaf(ppn, flags);
} }
/// The physical page number of a page table the kernel holds a reference to.
fn table_ppn(table: &PageTable) -> u64 {
(virt_to_phys(table as *const PageTable as usize) as u64) >> 12
}
pub fn map(root: &mut PageTable, mapping: Mapping<Sv39Flags>) { pub fn map(root: &mut PageTable, mapping: Mapping<Sv39Flags>) {
for i in 0..mapping.pages { for i in 0..mapping.pages {
let offset = i * PAGE_SIZE; let offset = i * PAGE_SIZE;
map_page(root, mapping.va.raw() + offset, mapping.pa.raw() + offset, mapping.flags); map_page(
root,
mapping.va.raw() + offset,
mapping.pa.raw() + offset,
mapping.flags,
);
} }
} }
pub fn init_kernel_page_table() -> &'static mut PageTable { pub fn init_kernel_page_table() -> &'static mut PageTable {
let root = alloc_page_table(); let root = alloc_page_table();
let identity = |addr: usize| (VirtualAddress::from_raw(addr), PhysicalAddress::from_raw(addr)); // Physical `pa` at its place in the upper-half direct map.
let direct = |pa: usize| {
(
VirtualAddress::from_raw(phys_to_virt(pa)),
PhysicalAddress::from_raw(pa),
)
};
let (va, pa) = identity(board::RAM_BASE); // The kernel image, then the rest of RAM, each with only the access it
map(root, Mapping { va, pa, pages: board::RAM_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rwx() }); // needs: code is never writable, data never executable. RAM below the
// image belongs to the firmware and stays unmapped, as does the guard
// page under each hart's kernel stack. Nothing is mapped in the lower
// half, which is userspace's alone.
let layout = KernelLayout::from_linker();
let ram_end = phys_to_virt(board::RAM_BASE + board::RAM_SIZE);
let stacks = (0..smp::MAX_HARTS).map(|h| {
let slot = layout.stacks_start + h * smp::STACK_SLOT;
(
slot + PAGE_SIZE,
slot + smp::STACK_SLOT,
Sv39Flags::kernel_rw(),
)
});
for (start, end, flags) in [
(layout.text_start, layout.text_end, Sv39Flags::kernel_rx()),
(layout.text_end, layout.rodata_end, Sv39Flags::kernel_ro()),
(
layout.rodata_end,
layout.stacks_start,
Sv39Flags::kernel_rw(),
),
(layout.stacks_end, ram_end, Sv39Flags::kernel_rw()),
]
.into_iter()
.chain(stacks)
{
let (va, pa) = direct(virt_to_phys(start));
map(
root,
Mapping {
va,
pa,
pages: (end - start) / PAGE_SIZE,
flags,
},
);
}
let (va, pa) = identity(board::UART_BASE); for (base, size) in [
map(root, Mapping { va, pa, pages: 1, flags: Sv39Flags::kernel_rw() }); (board::UART_BASE, PAGE_SIZE),
(board::PLIC_BASE, 0x40_0000),
let (va, pa) = identity(board::PLIC_BASE); (board::CLINT_BASE, 0x1_0000),
map(root, Mapping { va, pa, pages: 0x400000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() }); (board::VIRTIO_BASE, board::VIRTIO_SIZE),
] {
let (va, pa) = identity(board::CLINT_BASE); let (va, pa) = direct(base);
map(root, Mapping { va, pa, pages: 0x10000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() }); map(
root,
let (va, pa) = identity(board::VIRTIO_BASE); Mapping {
map(root, Mapping { va, pa, pages: board::VIRTIO_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rw() }); va,
pa,
pages: size / PAGE_SIZE,
flags: Sv39Flags::kernel_rw(),
},
);
}
root root
} }
/// Page-aligned boundaries of the kernel image, from linker.ld.
pub struct KernelLayout {
pub text_start: usize,
pub text_end: usize,
pub rodata_end: usize,
pub stacks_start: usize,
pub stacks_end: usize,
}
impl KernelLayout {
pub fn from_linker() -> Self {
unsafe extern "C" {
static _text_start: u8;
static _text_end: u8;
static _rodata_end: u8;
static _stacks_start: u8;
static _stacks_end: u8;
}
// SAFETY: only the symbols' addresses are taken, never their values.
unsafe {
Self {
text_start: &_text_start as *const u8 as usize,
text_end: &_text_end as *const u8 as usize,
rodata_end: &_rodata_end as *const u8 as usize,
stacks_start: &_stacks_start as *const u8 as usize,
stacks_end: &_stacks_end as *const u8 as usize,
}
}
}
}
/// What `check_wx` found wrong with a kernel page table, if anything.
pub enum WxViolation {
/// A page that is both writable and executable.
WritableCode(usize),
/// An executable page outside the kernel's text.
ExecutableData(usize),
/// A mapped page under the firmware, or the stack guard.
MappedHole(usize),
/// A kernel mapping in the lower half, which belongs to userspace.
LowerHalf(usize),
}
/// Walks every leaf of `root` and checks the kernel layout: nothing is
/// both writable and executable, only kernel text is executable, the
/// firmware and every stack guard are unmapped, and nothing at all is mapped
/// in the lower half. Returns the number of executable pages.
pub fn check_wx(root: &PageTable) -> Result<usize, WxViolation> {
let layout = KernelLayout::from_linker();
let text = layout.text_start..layout.text_end;
let is_guard = |va: usize| {
(layout.stacks_start..layout.stacks_end).contains(&va)
&& (va - layout.stacks_start).is_multiple_of(smp::STACK_SLOT)
};
let firmware = phys_to_virt(board::RAM_BASE)..layout.text_start;
let mut executable = 0;
for (va, bits) in leaves(root) {
if va < phys_to_virt(0) {
return Err(WxViolation::LowerHalf(va));
}
if bits & PTE_W != 0 && bits & PTE_X != 0 {
return Err(WxViolation::WritableCode(va));
}
if bits & PTE_X != 0 {
if !text.contains(&va) {
return Err(WxViolation::ExecutableData(va));
}
executable += 1;
}
if is_guard(va) || firmware.contains(&va) {
return Err(WxViolation::MappedHole(va));
}
}
Ok(executable)
}
/// Every 4 KiB leaf in `root` as (virtual address, PTE flag bits).
/// The kernel maps nothing larger.
fn leaves(root: &PageTable) -> impl Iterator<Item = (usize, u64)> + '_ {
let next = |e: &PageTableEntry| table_at((e.ppn() << 12) as usize) as &PageTable;
root.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.flat_map(move |(i2, e2)| {
next(e2)
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.flat_map(move |(i1, e1)| {
next(e1)
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.is_valid())
.map(move |(i0, e0)| {
let va = (i2 << 30) | (i1 << 21) | (i0 << 12);
// Sv39 addresses are sign extended from bit 38.
let va = if i2 >= 256 { va | !((1 << 39) - 1) } else { va };
(va, e0.0 & 0x3ff)
})
})
})
}
pub fn activate(root: &PageTable) { pub fn activate(root: &PageTable) {
let ppn = (root as *const PageTable as u64) >> 12; smp::note_active_root(virt_to_phys(root as *const PageTable as usize));
let satp = (8u64 << 60) | ppn; let satp = (8u64 << 60) | table_ppn(root);
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"csrw satp, {0}", "csrw satp, {0}",
@@ -176,34 +325,105 @@ pub fn activate(root: &PageTable) {
} }
} }
pub fn create_user_page_table( // ---- user address spaces ----
kernel_root: &PageTable, //
_program_pa: PhysicalAddress, // User mappings live in [USER_VA_START, USER_VA_END), which is root slot 0,
_program_pages: usize, // the L1 slots below USER_VA_END. Every address space gets its own root and its
_user_va: VirtualAddress, // own L1 for slot 0, and the kernel maps nothing in the lower half, so all
) -> &'static mut PageTable { // page tables below the user window are private to one process. The
let root = alloc_page_table(); // kernel's upper-half entries are shared by copying them.
for i in 0..512 { pub const USER_VA_START: usize = 0x1000;
if !kernel_root.entries[i].is_valid() { pub const USER_VA_END: usize = 0x0200_0000;
continue;
}
if i == 0 {
// User mappings land in VPN[2]=0 — each process needs its own L1
let kernel_l1_addr = (kernel_root.entries[i].ppn() << 12) as *const PageTable;
let kernel_l1 = unsafe { &*kernel_l1_addr };
let new_l1 = alloc_page_table();
for j in 0..512 {
if kernel_l1.entries[j].is_valid() {
new_l1.entries[j] = kernel_l1.entries[j];
}
}
let ppn = (new_l1 as *const PageTable as u64) >> 12;
root.entries[i] = PageTableEntry::new_branch(ppn);
} else {
root.entries[i] = kernel_root.entries[i];
}
}
root /// The page table at physical address `pa`, through the direct map.
pub fn table_at(pa: usize) -> &'static mut PageTable {
// SAFETY: every page table lives in RAM, which the kernel maps at
// `phys_to_virt`.
unsafe { &mut *(phys_to_virt(pa) as *mut PageTable) }
}
fn fresh_table(frames: &mut dyn FrameSource) -> Result<usize, MapError> {
let pa = frames
.alloc_table_frame()
.ok_or(MapError::OutOfFrames)?
.raw();
table_at(pa).zero();
Ok(pa)
}
fn in_user_window(va: usize) -> bool {
va.is_multiple_of(PAGE_SIZE) && (USER_VA_START..USER_VA_END).contains(&va)
}
/// Builds a root table sharing the kernel's mappings, with a private L1 for
/// the user window. Returns the root's physical address.
pub fn create_user_root(
kernel_root: &PageTable,
frames: &mut dyn FrameSource,
) -> Result<usize, MapError> {
let root_pa = fresh_table(frames)?;
let l1_pa = fresh_table(frames)?;
let root = table_at(root_pa);
for (mine, kernels) in root.entries.iter_mut().zip(&kernel_root.entries).skip(256) {
*mine = *kernels;
}
root.entries[0] = PageTableEntry::new_branch((l1_pa as u64) >> 12);
Ok(root_pa)
}
fn user_leaf_table(
root_pa: usize,
va: usize,
frames: Option<&mut dyn FrameSource>,
) -> Result<&'static mut PageTable, MapError> {
let root = table_at(root_pa);
let l1 = table_at((root.entries[0].ppn() << 12) as usize);
let slot = &mut l1.entries[vpn(va, 1)];
if !slot.is_valid() {
let frames = frames.ok_or(MapError::NoAddressSpace)?;
let l0_pa = fresh_table(frames)?;
*slot = PageTableEntry::new_branch((l0_pa as u64) >> 12);
}
Ok(table_at((slot.ppn() << 12) as usize))
}
pub fn map_user_page(
root_pa: usize,
va: usize,
pa: usize,
flags: Sv39Flags,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
if !in_user_window(va) || !pa.is_multiple_of(PAGE_SIZE) {
return Err(MapError::InvalidAddress);
}
let l0 = user_leaf_table(root_pa, va, Some(frames))?;
l0.entries[vpn(va, 0)] = PageTableEntry::new_leaf((pa as u64) >> 12, flags);
flush_tlb(va);
Ok(())
}
pub fn unmap_user_page(root_pa: usize, va: usize) {
if !in_user_window(va) {
return;
}
if let Ok(l0) = user_leaf_table(root_pa, va, None) {
l0.entries[vpn(va, 0)] = PageTableEntry::empty();
flush_tlb(va);
}
}
fn flush_tlb(va: usize) {
// SAFETY: sfence.vma only invalidates translation caches.
unsafe { core::arch::asm!("sfence.vma {0}, zero", in(reg) va) };
}
/// Physical address of the root table the hart is translating with.
pub fn active_root() -> usize {
let satp: usize;
// SAFETY: reading satp has no side effects.
unsafe { core::arch::asm!("csrr {0}, satp", out(reg) satp) };
(satp & ((1 << 44) - 1)) << 12
} }

View File

@@ -1,29 +1,29 @@
use domain::memory::MemoryRegion;
use domain::process::MAX_PROCESSES; use domain::process::MAX_PROCESSES;
use platform::Platform; use platform::{FrameSource, MapError, Platform};
use types::{ use types::{
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress}, address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
bounded::PageCount,
id::{CoreId, IrqId, ProcessId}, id::{CoreId, IrqId, ProcessId},
permission::{Permission, PermissionSet}, mapping::PagePerms,
time::{Ticks, TimerDuration}, time::{Ticks, TimerDuration},
}; };
use crate::board; use crate::board;
use crate::hw::{plic::Plic, timer}; use crate::hw::{plic::Plic, timer};
use crate::mmu; use crate::mmu;
use crate::smp;
use crate::trap; use crate::trap;
pub struct RiscVPlatform { pub struct RiscVPlatform {
num_cores: u8, num_cores: u8,
kernel_pt: usize, kernel_pt: usize,
/// Root table of each process's address space, 0 if it has none.
page_tables: [usize; MAX_PROCESSES], page_tables: [usize; MAX_PROCESSES],
} }
impl RiscVPlatform { impl RiscVPlatform {
pub fn new(kernel_pt_addr: usize) -> Self { pub fn new(kernel_pt_addr: usize, num_cores: u8) -> Self {
Self { Self {
num_cores: 1, num_cores,
kernel_pt: kernel_pt_addr, kernel_pt: kernel_pt_addr,
page_tables: [0usize; MAX_PROCESSES], page_tables: [0usize; MAX_PROCESSES],
} }
@@ -33,37 +33,82 @@ impl RiscVPlatform {
self.page_tables[pid.raw() as usize] self.page_tables[pid.raw() as usize]
} }
pub fn set_page_table(&mut self, pid: ProcessId, addr: usize) { fn root(&self, pid: ProcessId) -> Option<usize> {
self.page_tables[pid.raw() as usize] = addr; match self.page_tables.get(pid.raw() as usize) {
} Some(&0) | None => None,
Some(&root) => Some(root),
fn ensure_page_table(&mut self, pid: ProcessId) -> &mut mmu::PageTable {
let idx = pid.raw() as usize;
if self.page_tables[idx] == 0 {
let kernel_pt = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
let user_pt = mmu::create_user_page_table(
kernel_pt,
PhysicalAddress::from_raw(0),
0,
VirtualAddress::from_raw(0x10000),
);
self.page_tables[idx] = user_pt as *const mmu::PageTable as usize;
} }
unsafe { &mut *(self.page_tables[idx] as *mut mmu::PageTable) }
} }
} }
impl Platform for RiscVPlatform { impl Platform for RiscVPlatform {
fn configure_protection(&mut self, process: ProcessId, regions: &[MemoryRegion]) { const USER_VA_START: usize = mmu::USER_VA_START;
let pt = self.ensure_page_table(process); const USER_VA_END: usize = mmu::USER_VA_END;
for region in regions {
mmu::map(pt, mmu::Mapping { fn create_address_space(
va: VirtualAddress::from_raw(region.base().raw()), &mut self,
pa: region.base(), process: ProcessId,
pages: region.size().value() as usize, frames: &mut dyn FrameSource,
flags: mmu::Sv39Flags::user(true, true, true), ) -> Result<(), MapError> {
}); let slot = self
.page_tables
.get_mut(process.raw() as usize)
.ok_or(MapError::NoAddressSpace)?;
// SAFETY: the kernel page table lives in the boot heap forever.
let kernel_root = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
*slot = mmu::create_user_root(kernel_root, frames)?;
Ok(())
}
fn destroy_address_space(&mut self, process: ProcessId) {
let Some(root) = self.root(process) else {
return;
};
// No hart may still translate through it once the kernel frees its
// frames: other harts leave it (and wait for nothing but that), and
// the caller may be tearing down the space we are running on.
smp::shoot_down(root);
if mmu::active_root() == root {
// SAFETY: the kernel page table lives in the boot heap forever.
mmu::activate(unsafe { &*(self.kernel_pt as *const mmu::PageTable) });
} }
self.page_tables[process.raw() as usize] = 0;
// SAFETY: flushing translations has no other effect.
unsafe { core::arch::asm!("sfence.vma zero, zero") };
}
fn map_page(
&mut self,
process: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
perms: PagePerms,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
let root = self.root(process).ok_or(MapError::NoAddressSpace)?;
mmu::map_user_page(
root,
va.raw(),
pa.raw(),
mmu::Sv39Flags::user(perms),
frames,
)
}
fn unmap_page(&mut self, process: ProcessId, va: VirtualAddress) {
if let Some(root) = self.root(process) {
mmu::unmap_user_page(root, va.raw());
// Other harts running this process may still cache it.
smp::shoot_down(root);
}
}
fn zero_frame(&mut self, pa: PhysicalAddress) {
// SAFETY: pool frames are RAM, mapped at `phys_to_virt`, and the
// kernel only zeroes frames it has just allocated.
unsafe {
core::ptr::write_bytes(mmu::phys_to_virt(pa.raw()) as *mut u8, 0, mmu::PAGE_SIZE)
};
} }
fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> ! { fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> ! {
@@ -104,68 +149,34 @@ impl Platform for RiscVPlatform {
Plic::enable(irq.raw() as u32); Plic::enable(irq.raw() as u32);
} }
fn send_ipi(&mut self, _target: CoreId) {} fn send_ipi(&mut self, target: CoreId) {
smp::kick(target.raw() as usize);
}
fn core_id(&self) -> CoreId { fn core_id(&self) -> CoreId {
CoreId::from_raw(0) CoreId::from_raw(smp::hart_id() as u8)
} }
fn num_cores(&self) -> u8 { fn num_cores(&self) -> u8 {
self.num_cores self.num_cores
} }
fn map_shared(
&mut self,
process: ProcessId,
base: PhysicalAddress,
size: PageCount,
permissions: PermissionSet,
) {
let pt = self.ensure_page_table(process);
mmu::map(pt, mmu::Mapping {
va: VirtualAddress::from_raw(base.raw()),
pa: base,
pages: size.value() as usize,
flags: mmu::Sv39Flags::user(
permissions.allows(Permission::Read),
permissions.allows(Permission::Write),
permissions.allows(Permission::Execute),
),
});
}
fn debug_putchar(&self, byte: u8) { fn debug_putchar(&self, byte: u8) {
crate::hw::uart::Uart::new().put_char(byte); crate::hw::uart::Uart::new().put_char(byte);
} }
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
let pt_addr = self.page_tables[process.raw() as usize];
if pt_addr == 0 { return None; }
let pt = unsafe { &*(pt_addr as *const mmu::PageTable) };
mmu::translate(pt, va.raw()).map(PhysicalAddress::from_raw)
}
fn map_user_page(&mut self, process: ProcessId, va: VirtualAddress, pa: PhysicalAddress, writable: bool, executable: bool) {
let pt = self.ensure_page_table(process);
let flags = mmu::Sv39Flags::user(true, writable, executable);
mmu::map(pt, mmu::Mapping {
va,
pa,
pages: 1,
flags,
});
}
fn halt(&self) -> ! { fn halt(&self) -> ! {
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"ecall", "ecall",
in("a0") 0usize, inlateout("a0") 0usize => _,
in("a1") 0usize, inlateout("a1") 0usize => _,
in("a7") 0x53525354usize, in("a7") 0x53525354usize,
in("a6") 0usize, in("a6") 0usize,
); );
} }
loop { unsafe { core::arch::asm!("wfi") }; } loop {
unsafe { core::arch::asm!("wfi") };
}
} }
} }

View File

@@ -0,0 +1,220 @@
//! Harts. Each hart has its own kernel stack, and its hart id doubles as
//! its kernel `CoreId`. The kernel is entered under one lock for now (stage
//! 1 of multi-core; the kernel itself stays a sequential program), so
//! userspace runs in parallel and the kernel does not.
//!
//! Harts talk through IPIs carrying two kinds of request:
//! - reschedule: the kernel made work for this hart, or took its thread
//! away; handled under the lock on the way out of the kernel.
//! - leave root R: a page table this hart may be using is changing or about
//! to be freed; the hart switches to the kernel's root and flushes its
//! TLB. This needs no lock, so it is serviced even while a hart spins
//! waiting for the lock or idles, and the sender (which holds the lock)
//! can wait for the acknowledgement without deadlocking.
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
use crate::mmu;
/// Must match linker.ld.
pub const MAX_HARTS: usize = 8;
/// A guard page and a 512 KiB stack (boot builds big tables on it). Must
/// match linker.ld.
pub const STACK_SLOT: usize = 4096 + 0x8_0000;
const SIP_SSIP: usize = 1 << 1;
/// The hart running this code, from which kernel stack it is on.
pub fn hart_id() -> usize {
let sp: usize;
// SAFETY: reading sp has no side effects.
unsafe { core::arch::asm!("mv {0}, sp", out(reg) sp) };
(sp - stacks_start()) / STACK_SLOT
}
pub fn stacks_start() -> usize {
unsafe extern "C" {
static _stacks_start: u8;
}
// SAFETY: only the symbol's address is taken.
unsafe { &_stacks_start as *const u8 as usize }
}
static BOOT_HART: AtomicUsize = AtomicUsize::new(0);
static NUM_HARTS: AtomicUsize = AtomicUsize::new(1);
/// Physical address of the kernel's own root table.
static KERNEL_ROOT: AtomicUsize = AtomicUsize::new(0);
/// The root table each hart translates with.
static ACTIVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
static RESCHEDULE: [AtomicBool; MAX_HARTS] = [const { AtomicBool::new(false) }; MAX_HARTS];
static LEAVE_ROOT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
static LEFT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(0) }; MAX_HARTS];
pub fn boot_hart() -> usize {
BOOT_HART.load(Ordering::Relaxed)
}
pub fn num_harts() -> usize {
NUM_HARTS.load(Ordering::Relaxed)
}
pub fn kernel_root() -> usize {
KERNEL_ROOT.load(Ordering::Relaxed)
}
/// Records which hart booted and how many there are, and where the
/// kernel's root table is.
pub fn init(boot: usize, harts: usize, kernel_root: usize) {
BOOT_HART.store(boot, Ordering::Relaxed);
NUM_HARTS.store(harts, Ordering::Relaxed);
KERNEL_ROOT.store(kernel_root, Ordering::Relaxed);
}
/// Called by `mmu::activate`.
pub fn note_active_root(root: usize) {
if let Some(r) = ACTIVE_ROOT.get(hart_id()) {
r.store(root, Ordering::SeqCst);
}
}
// ---- the kernel lock ----
static NEXT_TICKET: AtomicU32 = AtomicU32::new(0);
static SERVING: AtomicU32 = AtomicU32::new(0);
/// Takes the kernel lock, first come first served. Leave-root requests
/// are serviced while waiting.
pub fn lock() {
let ticket = NEXT_TICKET.fetch_add(1, Ordering::Relaxed);
while SERVING.load(Ordering::Acquire) != ticket {
service_ipi();
core::hint::spin_loop();
}
}
pub fn unlock() {
SERVING.fetch_add(1, Ordering::Release);
}
// ---- IPIs ----
/// The lock-free half of an IPI: acknowledges it and, if asked, leaves the
/// root being changed. Safe to call at any time, any number of times.
pub fn service_ipi() {
let sip: usize;
// SAFETY: reading sip has no side effects.
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
if sip & SIP_SSIP == 0 {
return;
}
// Clear first: a request posted after this raises SSIP again.
// SAFETY: clearing our own pending software interrupt.
unsafe { core::arch::asm!("csrc sip, {0}", in(reg) SIP_SSIP) };
let me = hart_id();
let (Some(leave), Some(left)) = (LEAVE_ROOT.get(me), LEFT.get(me)) else {
return;
};
let root = leave.swap(0, Ordering::SeqCst);
if root != 0 {
if ACTIVE_ROOT.get(me).map(|r| r.load(Ordering::SeqCst)) == Some(root) {
mmu::activate(mmu::page_table::table_at(kernel_root()));
}
// SAFETY: flushing translations has no other effect.
unsafe { core::arch::asm!("sfence.vma zero, zero") };
left.fetch_add(1, Ordering::SeqCst);
}
}
/// Asks `hart` to look at its run queue again, on its way out of the
/// kernel.
pub fn kick(hart: usize) {
if let Some(r) = RESCHEDULE.get(hart) {
r.store(true, Ordering::SeqCst);
sbi_send_ipi(hart);
}
}
/// Consumes this hart's reschedule request.
pub fn take_reschedule() -> bool {
RESCHEDULE
.get(hart_id())
.is_some_and(|r| r.swap(false, Ordering::SeqCst))
}
/// Makes every other hart that translates through `root` leave it and
/// flush, and waits until each has. Called with the kernel lock held, so
/// no hart can start using `root` meanwhile.
pub fn shoot_down(root: usize) {
let me = hart_id();
for hart in 0..num_harts() {
if hart == me || ACTIVE_ROOT[hart].load(Ordering::SeqCst) != root {
continue;
}
let seen = LEFT[hart].load(Ordering::SeqCst);
LEAVE_ROOT[hart].store(root, Ordering::SeqCst);
sbi_send_ipi(hart);
while LEFT[hart].load(Ordering::SeqCst) == seen {
core::hint::spin_loop();
}
}
}
// ---- SBI ----
const SBI_EXT_IPI: usize = 0x735049;
const SBI_EXT_HSM: usize = 0x48534D;
fn sbi_send_ipi(hart: usize) {
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") 1usize => _,
inlateout("a1") hart => _,
in("a6") 0usize,
in("a7") SBI_EXT_IPI,
);
}
}
/// Whether `hart` exists, per SBI HSM `hart_get_status`.
fn sbi_hart_exists(hart: usize) -> bool {
let error: isize;
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") hart => error,
lateout("a1") _,
in("a6") 2usize,
in("a7") SBI_EXT_HSM,
);
}
error == 0
}
/// The number of harts, counting up from hart 0 until one is missing.
pub fn probe_harts() -> usize {
(0..MAX_HARTS)
.take_while(|h| sbi_hart_exists(*h))
.count()
.max(1)
}
/// Starts `hart` at the physical address `entry` with the MMU off.
pub fn sbi_hart_start(hart: usize, entry: usize) -> bool {
let error: isize;
// SAFETY: an SBI call; a0 and a1 come back clobbered.
unsafe {
core::arch::asm!(
"ecall",
inlateout("a0") hart => error,
inlateout("a1") entry => _,
in("a2") 0usize,
in("a6") 0usize,
in("a7") SBI_EXT_HSM,
);
}
error == 0
}

View File

@@ -7,17 +7,9 @@ pub struct TrapFrame {
pub sstatus: usize, pub sstatus: usize,
} }
impl TrapFrame { global_asm!(
pub fn new() -> Self { r#"
Self { .section .text
regs: [0; 32],
sepc: 0,
sstatus: 0,
}
}
}
global_asm!(r#"
.align 4 .align 4
.globl trap_vector .globl trap_vector
trap_vector: trap_vector:
@@ -68,7 +60,14 @@ _from_user:
mv a0, sp mv a0, sp
call trap_handler_rust call trap_handler_rust
j _restore_frame
# trap_return(frame): resume whatever `frame` describes, as if returning
# from a trap that saved it. Used to enter userspace for the first time.
.globl trap_return
trap_return:
mv sp, a0
_restore_frame:
ld t0, 256(sp) ld t0, 256(sp)
csrw sepc, t0 csrw sepc, t0
ld t0, 264(sp) ld t0, 264(sp)
@@ -84,7 +83,6 @@ _restore_smode:
ld x1, 8(sp) ld x1, 8(sp)
ld x3, 24(sp) ld x3, 24(sp)
ld x4, 32(sp) ld x4, 32(sp)
ld x5, 40(sp)
ld x6, 48(sp) ld x6, 48(sp)
ld x7, 56(sp) ld x7, 56(sp)
ld x8, 64(sp) ld x8, 64(sp)
@@ -112,27 +110,37 @@ _restore_smode:
ld x30, 240(sp) ld x30, 240(sp)
ld x31, 248(sp) ld x31, 248(sp)
addi sp, sp, 272 # Every GPR except t0 (x5) is restored. Decide where we return to while
# t0 is still free, and restore it last on each path; using t0 after
# its restore would corrupt the interrupted program's t0.
csrr t0, sstatus csrr t0, sstatus
andi t0, t0, 0x100 andi t0, t0, 0x100
bnez t0, _sret_smode bnez t0, _sret_smode
ld x5, 40(sp)
addi sp, sp, 272
csrrw sp, sscratch, sp csrrw sp, sscratch, sp
_sret_smode:
sret sret
"#); _sret_smode:
ld x5, 40(sp)
addi sp, sp, 272
sret
"#
);
const CAUSE_INTERRUPT_BIT: usize = 1 << 63; const CAUSE_INTERRUPT_BIT: usize = 1 << 63;
const CAUSE_S_SOFTWARE: usize = 1;
const CAUSE_S_TIMER: usize = 5; const CAUSE_S_TIMER: usize = 5;
const CAUSE_S_EXTERNAL: usize = 9; const CAUSE_S_EXTERNAL: usize = 9;
const CAUSE_ECALL_U: usize = 8; const CAUSE_ECALL_U: usize = 8;
/// Set in `sstatus` when the trap came from supervisor mode.
const SSTATUS_SPP: usize = 1 << 8;
pub fn init() { pub fn init() {
unsafe extern "C" { unsafe extern "C" {
fn trap_vector(); fn trap_vector();
} }
unsafe { unsafe {
let addr = trap_vector as usize; let addr = trap_vector as *const () as usize;
core::arch::asm!("csrw stvec, {0}", in(reg) addr); core::arch::asm!("csrw stvec, {0}", in(reg) addr);
core::arch::asm!("csrw sscratch, zero"); core::arch::asm!("csrw sscratch, zero");
// SIE stays clear — interrupts are enabled only when entering userspace via sret // SIE stays clear — interrupts are enabled only when entering userspace via sret
@@ -147,133 +155,18 @@ pub fn set_kernel_stack(kernel_sp: usize) {
} }
} }
fn decode_syscall(frame: &TrapFrame) -> Option<domain::syscall::Syscall> { fn args(frame: &TrapFrame) -> [usize; 6] {
use domain::message::Message; let mut a = [0usize; 6];
use domain::syscall::{Syscall, WaitSet}; a.copy_from_slice(&frame.regs[10..16]);
use types::syscall_abi::*; a
let nr = frame.regs[17]; // a7
let a0 = frame.regs[10];
let a1 = frame.regs[11];
let a2 = frame.regs[12];
let a3 = frame.regs[13];
let a4 = frame.regs[14];
match nr {
SYS_EXIT => Some(Syscall::Exit),
SYS_HALT => Some(Syscall::Halt),
SYS_EXIT_THREAD => Some(Syscall::ExitThread),
SYS_YIELD => Some(Syscall::Yield),
SYS_GET_TIME => Some(Syscall::GetTime),
SYS_GET_PROCESS_ID => Some(Syscall::GetProcessId),
SYS_SEND => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
let msg = Message::from_words([a1, a2, a3, a4]);
Some(Syscall::Send { endpoint: slot, message: msg })
}
SYS_RECEIVE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Receive { endpoint: slot })
}
SYS_DELEGATE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
let to = types::bounded::CapSlot::new(a1 as u8)?;
let perms = types::permission::PermissionSet::from_bits(a2 as u8);
Some(Syscall::Delegate { slot, to_endpoint: to, permissions: perms })
}
SYS_WAIT_FOR_INTERRUPT => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::WaitForInterrupt { irq: slot })
}
SYS_ACKNOWLEDGE_INTERRUPT => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::AcknowledgeInterrupt { irq: slot })
}
SYS_WAIT_ANY => {
let ep0 = if a0 != usize::MAX { types::bounded::CapSlot::new(a0 as u8) } else { None };
let ep1 = if a1 != usize::MAX { types::bounded::CapSlot::new(a1 as u8) } else { None };
let ep2 = if a2 != usize::MAX { types::bounded::CapSlot::new(a2 as u8) } else { None };
let ep3 = if a3 != usize::MAX { types::bounded::CapSlot::new(a3 as u8) } else { None };
let sources = WaitSet::new(
[ep0, ep1, ep2, ep3],
[None, None, None, None],
);
Some(Syscall::WaitAny { sources })
}
SYS_CREATE_PROCESS => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::CreateProcess {
create_cap: slot,
image: types::address::DataRef::empty(),
})
}
SYS_KILL => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Kill { process: slot })
}
SYS_REVOKE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::Revoke { slot })
}
SYS_SPAWN_THREAD => Some(Syscall::SpawnThread {
entry: types::address::EntryPoint::from_raw(a0),
stack: types::address::StackPointer::from_raw(a1),
}),
SYS_REQUEST_PAGES => {
let count = types::bounded::PageCount::new(a0 as u16)?;
Some(Syscall::RequestPages { count })
}
SYS_RELEASE_PAGES => {
let count = types::bounded::PageCount::new(a1 as u16)?;
Some(Syscall::ReleasePages {
addr: types::address::VirtualAddress::from_raw(a0),
count,
})
}
SYS_SPAWN_THREAD_IN => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::SpawnThreadIn {
process: slot,
entry: types::address::EntryPoint::from_raw(a1),
stack: types::address::StackPointer::from_raw(a2),
})
}
SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint),
SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }),
SYS_MAP_PAGE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?;
Some(Syscall::MapPage {
process: slot,
src_va: types::address::VirtualAddress::from_raw(a1),
dst_va: types::address::VirtualAddress::from_raw(a2),
executable: a3 != 0,
})
}
_ => None,
}
} }
fn write_result(frame: &mut TrapFrame, outcome: &kernel::SyscallOutcome) { fn set_results(frame: &mut TrapFrame, regs: [usize; 8]) {
use kernel::{SyscallOutcome, SyscallResult}; frame.regs[10..18].copy_from_slice(&regs);
}
match outcome { fn write_result(frame: &mut TrapFrame, result: &kernel::SyscallResult) {
SyscallOutcome::Complete(result) => match result { set_results(frame, kernel::abi::encode(result));
SyscallResult::Ok => frame.regs[10] = 0,
SyscallResult::Value(v) => frame.regs[10] = *v,
SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize,
SyscallResult::Message(m) => {
frame.regs[10] = m.word(0);
frame.regs[11] = m.word(1);
frame.regs[12] = m.word(2);
frame.regs[13] = m.word(3);
}
SyscallResult::Error(_) => frame.regs[10] = usize::MAX,
SyscallResult::WakeReason(_) => frame.regs[10] = 0,
},
SyscallOutcome::BlockCaller(_) => {}
SyscallOutcome::SwitchTo(_) => frame.regs[10] = 0,
SyscallOutcome::TerminateCaller => {}
}
} }
fn save_frame_to_context(frame: &TrapFrame, ctx: &mut domain::thread::SavedContext) { fn save_frame_to_context(frame: &TrapFrame, ctx: &mut domain::thread::SavedContext) {
@@ -288,161 +181,224 @@ fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFra
frame.sstatus = ctx.status(); frame.sstatus = ctx.status();
// ensure returning to U-mode // ensure returning to U-mode
frame.sstatus &= !(1 << 8); // clear SPP frame.sstatus &= !(1 << 8); // clear SPP
frame.sstatus |= 1 << 5; // set SPIE frame.sstatus |= 1 << 5; // set SPIE
} }
fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) { fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) {
let kern = unsafe { &mut *crate::kernel_ptr() }; if let Some(result) = kern().take_pending_result(tid) {
if let Some(result) = kern.take_pending_result(tid) { write_result(frame, &result);
write_result_direct(frame, &result);
} }
} }
fn write_result_direct(frame: &mut TrapFrame, result: &kernel::SyscallResult) { unsafe extern "C" {
use kernel::SyscallResult; fn trap_return(frame: *mut TrapFrame) -> !;
match result {
SyscallResult::Ok => frame.regs[10] = 0,
SyscallResult::Value(v) => frame.regs[10] = *v,
SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize,
SyscallResult::Message(m) => {
frame.regs[10] = m.word(0);
frame.regs[11] = m.word(1);
frame.regs[12] = m.word(2);
frame.regs[13] = m.word(3);
}
SyscallResult::Error(_) => frame.regs[10] = usize::MAX,
SyscallResult::WakeReason(_) => frame.regs[10] = 0,
}
} }
fn switch_to_thread(frame: &mut TrapFrame, current_tid: types::id::ThreadId, next_tid: types::id::ThreadId) { fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
let kern = unsafe { &mut *crate::kernel_ptr() }; // SAFETY: only called with the kernel lock held.
unsafe { &mut *crate::kernel_ptr() }
}
if let Some(ctx) = kern.thread_context_mut(current_tid) { fn this_core() -> types::id::CoreId {
types::id::CoreId::from_raw(crate::smp::hart_id() as u8)
}
/// Saves `frame` into `from` (if it still exists) and loads `to`.
fn switch_to_thread(
frame: &mut TrapFrame,
from: Option<types::id::ThreadId>,
to: types::id::ThreadId,
) {
let kern = kern();
if let Some(ctx) = from.and_then(|t| kern.thread_context_mut(t)) {
save_frame_to_context(frame, ctx); save_frame_to_context(frame, ctx);
} }
if let Some(ctx) = kern.thread_context(to) {
let next_pid = kern.thread_process(next_tid);
let current_pid = kern.thread_process(current_tid);
if let Some(ctx) = kern.thread_context(next_tid) {
load_context_to_frame(ctx, frame); load_context_to_frame(ctx, frame);
} }
apply_pending_result(frame, to);
}
apply_pending_result(frame, next_tid); /// Saves `from` and runs the next ready thread, idling until there is one.
fn switch_to_next(frame: &mut TrapFrame, from: Option<types::id::ThreadId>) {
if let Some(ctx) = from.and_then(|t| kern().thread_context_mut(t)) {
save_frame_to_context(frame, ctx);
}
let next = next_runnable();
let _ = kern().set_thread_running(next, this_core());
switch_to_thread(frame, None, next);
}
if next_pid != current_pid { /// The next thread to run on this hart, idling until one is ready.
if let Some(pid) = next_pid { ///
let pt_addr = kern.platform_mut().page_table_for(pid); /// Idling gives up the kernel lock: an idle hart holds nothing, so other
if pt_addr != 0 { /// harts can make work for it (and kick it) meanwhile. It also leaves any
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) }; /// user address space first, so nobody needs to shoot it down. SIE stays
crate::mmu::activate(pt); /// clear; `wfi` still wakes on a pending interrupt, serviced by polling.
} fn next_runnable() -> types::id::ThreadId {
loop {
if let Some(tid) = kern().pick_next_thread() {
return tid;
}
crate::mmu::activate(crate::mmu::page_table::table_at(crate::smp::kernel_root()));
crate::smp::unlock();
unsafe { core::arch::asm!("wfi") };
crate::smp::service_ipi();
crate::smp::lock();
let _ = crate::smp::take_reschedule();
let sip: usize;
unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) };
if sip & (1 << CAUSE_S_TIMER) != 0 {
// No thread is running, so there is no time slice to charge.
crate::hw::timer::handle_interrupt();
}
if sip & (1 << CAUSE_S_EXTERNAL) != 0 {
dispatch_external();
} }
} }
} }
fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) { /// Claims every pending PLIC interrupt and hands each to the kernel. Only
let kern = unsafe { &mut *crate::kernel_ptr() }; /// the boot hart receives external interrupts.
fn dispatch_external() {
if let Some(ctx) = kern.thread_context_mut(current_tid) { loop {
save_frame_to_context(frame, ctx); let irq_num = crate::hw::plic::Plic::claim();
} if irq_num == 0 {
return;
if let Some(next_tid) = kern.pick_next_thread() {
let next_pid = kern.thread_process(next_tid);
let current_pid = kern.thread_process(current_tid);
let core = types::id::CoreId::from_raw(0);
let _ = kern.set_thread_running(next_tid, core);
if let Some(ctx) = kern.thread_context(next_tid) {
load_context_to_frame(ctx, frame);
} }
kern().handle_interrupt(types::id::IrqId::from_raw(irq_num as u16));
apply_pending_result(frame, next_tid); crate::hw::plic::Plic::complete(irq_num);
if next_pid != current_pid {
if let Some(pid) = next_pid {
let pt_addr = kern.platform_mut().page_table_for(pid);
if pt_addr != 0 {
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) };
crate::mmu::activate(pt);
}
}
}
} else {
loop { unsafe { core::arch::asm!("wfi") }; }
} }
} }
/// Last step before leaving the kernel to userspace: act on a reschedule
/// request, make sure this hart has a thread (its own may have been taken
/// away by another hart), and translate through that thread's address
/// space.
fn finish(frame: &mut TrapFrame) {
let kern = kern();
let current = kern.current_thread();
if crate::smp::take_reschedule()
&& let Some(cur) = current
&& let domain::scheduler::SchedulerDecision::Preempt(next) = kern.reschedule(this_core())
{
switch_to_thread(frame, Some(cur), next);
}
if kern.current_thread().is_none() {
switch_to_next(frame, None);
}
let root = kern
.current_thread()
.and_then(|t| kern.thread_process(t))
.map(|pid| kern.platform_mut().page_table_for(pid))
.unwrap_or(0);
if root != 0 && crate::mmu::active_root() != root {
crate::mmu::activate(crate::mmu::page_table::table_at(root));
}
}
/// Leaves the kernel for userspace for the first time on this hart, with
/// the kernel lock held: runs the thread the kernel already made current
/// here (init, on the boot hart) or the next one homed here.
pub fn enter_first() -> ! {
let mut frame = TrapFrame {
regs: [0; 32],
sepc: 0,
sstatus: 0,
};
match kern().current_thread() {
Some(tid) => switch_to_thread(&mut frame, None, tid),
None => switch_to_next(&mut frame, None),
}
finish(&mut frame);
crate::smp::unlock();
// SAFETY: `frame` holds a user context; sscratch is set from it on
// the way out, so later traps land at the top of this frame.
unsafe { trap_return(&mut frame) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn trap_handler_rust(frame: &mut TrapFrame) { extern "C" fn trap_handler_rust(frame: &mut TrapFrame) {
let scause: usize; let scause: usize;
unsafe { core::arch::asm!("csrr {0}, scause", out(reg) scause) }; unsafe { core::arch::asm!("csrr {0}, scause", out(reg) scause) };
if frame.sstatus & SSTATUS_SPP != 0 {
// Checked before taking the lock: this hart may already hold it.
kernel_fault(frame, scause);
}
crate::smp::service_ipi();
crate::smp::lock();
if scause & CAUSE_INTERRUPT_BIT != 0 { if scause & CAUSE_INTERRUPT_BIT != 0 {
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT); handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
} else { } else {
handle_exception(frame, scause); handle_exception(frame, scause);
} }
finish(frame);
crate::smp::unlock();
} }
fn handle_interrupt(frame: &mut TrapFrame, cause: usize) { fn handle_interrupt(frame: &mut TrapFrame, cause: usize) {
match cause { match cause {
CAUSE_S_TIMER => { CAUSE_S_TIMER => {
crate::hw::timer::handle_interrupt(); crate::hw::timer::handle_interrupt();
let kern = unsafe { &mut *crate::kernel_ptr() }; let kern = kern();
let core = types::id::CoreId::from_raw(0);
if let Some(current_tid) = kern.current_thread() { if let Some(current_tid) = kern.current_thread() {
let decision = kern.handle_tick(core); let decision = kern.handle_tick(this_core());
if let domain::scheduler::SchedulerDecision::Preempt(next) = decision { if let domain::scheduler::SchedulerDecision::Preempt(next) = decision {
switch_to_thread(frame, current_tid, next); switch_to_thread(frame, Some(current_tid), next);
} }
} }
} }
CAUSE_S_EXTERNAL => { CAUSE_S_EXTERNAL => dispatch_external(),
let irq_num = crate::hw::plic::Plic::claim(); // Serviced on entry; any reschedule happens in `finish`.
if irq_num != 0 { CAUSE_S_SOFTWARE => {}
let kern = unsafe { &mut *crate::kernel_ptr() };
let irq = types::id::IrqId::from_raw(irq_num as u16);
kern.handle_interrupt(irq);
crate::hw::plic::Plic::complete(irq_num);
}
}
_ => {} _ => {}
} }
} }
/// The kernel itself faulted (a W^X violation, a stack guard, a bad
/// pointer). Its state can no longer be trusted, and no user thread is to
/// blame.
fn kernel_fault(frame: &TrapFrame, scause: usize) -> ! {
let uart = crate::hw::uart::Uart::new();
uart.write_str("KERNEL FAULT: scause=");
uart.write_hex(scause);
let stval: usize;
unsafe { core::arch::asm!("csrr {0}, stval", out(reg) stval) };
uart.write_str(" stval=");
uart.write_hex(stval);
uart.write_str(" sepc=");
uart.write_hex(frame.sepc);
uart.write_str(" hart=");
uart.write_dec(crate::smp::hart_id());
uart.put_char(b'\n');
crate::halt(&uart, "fault in kernel mode");
}
fn handle_exception(frame: &mut TrapFrame, scause: usize) { fn handle_exception(frame: &mut TrapFrame, scause: usize) {
match scause { match scause {
CAUSE_ECALL_U => { CAUSE_ECALL_U => {
frame.sepc += 4; frame.sepc += 4;
if let Some(syscall) = decode_syscall(frame) { let Some(syscall) = kernel::abi::decode(frame.regs[17], &args(frame)) else {
let kern = unsafe { &mut *crate::kernel_ptr() }; set_results(frame, kernel::abi::unknown_syscall());
return;
};
{
let kern = kern();
let current_tid = kern.current_thread(); let current_tid = kern.current_thread();
let outcome = kern.handle_syscall(syscall); let outcome = kern.handle_syscall(syscall);
write_result(frame, &outcome);
match outcome { match outcome {
kernel::SyscallOutcome::Complete(_) => {} kernel::SyscallOutcome::Complete(result) => write_result(frame, &result),
kernel::SyscallOutcome::BlockCaller(_) => { kernel::SyscallOutcome::BlockCaller(_) => switch_to_next(frame, current_tid),
if let Some(tid) = current_tid {
switch_to_next(frame, tid);
}
}
kernel::SyscallOutcome::SwitchTo(next) => { kernel::SyscallOutcome::SwitchTo(next) => {
if let Some(tid) = current_tid { switch_to_thread(frame, current_tid, next)
switch_to_thread(frame, tid, next);
}
} }
kernel::SyscallOutcome::TerminateCaller => { kernel::SyscallOutcome::TerminateCaller => {
let uart = crate::hw::uart::Uart::new(); let uart = crate::hw::uart::Uart::new();
uart.write_str("k-os: process exited\n"); uart.write_str("k-os: process exited\n");
if let Some(tid) = current_tid { switch_to_next(frame, current_tid);
switch_to_next(frame, tid);
}
} }
} }
} }
@@ -459,7 +415,7 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
uart.write_hex(frame.sepc); uart.write_hex(frame.sepc);
uart.put_char(b'\n'); uart.put_char(b'\n');
let kern = unsafe { &mut *crate::kernel_ptr() }; let kern = kern();
if let Some(current_tid) = kern.current_thread() { if let Some(current_tid) = kern.current_thread() {
let fault = match scause { let fault = match scause {
2 => domain::fault::Fault::IllegalInstruction, 2 => domain::fault::Fault::IllegalInstruction,
@@ -469,10 +425,10 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
_ => domain::fault::Fault::InvalidSyscall, _ => domain::fault::Fault::InvalidSyscall,
}; };
kern.handle_fault(current_tid, fault, frame.sepc); kern.handle_fault(current_tid, fault, frame.sepc);
switch_to_next(frame, current_tid); switch_to_next(frame, Some(current_tid));
} else {
loop { unsafe { core::arch::asm!("wfi") }; }
} }
// With no current thread (another hart just destroyed it),
// `finish` moves on to the next one.
} }
} }
} }

View File

@@ -4,7 +4,7 @@ const CELL_SIZE: u16 = 24;
const BG_COLOR: u32 = 0xFF202040; const BG_COLOR: u32 = 0xFF202040;
const SNAKE_COLOR: u32 = 0xFF00FF00; const SNAKE_COLOR: u32 = 0xFF00FF00;
const HEAD_COLOR: u32 = 0xFF00CC00; const HEAD_COLOR: u32 = 0xFF00CC00;
const FOOD_COLOR: u32 = 0xFFFF0000; pub(crate) const FOOD_COLOR: u32 = 0xFFFF0000;
const BORDER_COLOR: u32 = 0xFF505080; const BORDER_COLOR: u32 = 0xFF505080;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]

View File

@@ -28,5 +28,5 @@ fn food_is_rendered() {
let count = render(&snake, &mut commands); let count = render(&snake, &mut commands);
// Food is command index 2 (after bg and border) // Food is command index 2 (after bg and border)
assert!(count > 2); assert!(count > 2);
assert_eq!(commands[2].color, 0xFF0040); assert_eq!(commands[2].color, crate::renderer::FOOD_COLOR);
} }

View File

@@ -5,3 +5,6 @@ edition = "2024"
[dependencies] [dependencies]
types = { workspace = true } types = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }

View File

@@ -0,0 +1,88 @@
//! The capability derivation relation, reduced to its essence: every
//! capability is a node with at most one parent. Kept free of kernel
//! types so the revocation rule can be checked on its own.
/// `result[i]` is true iff `root` is `i` or an ancestor of `i`.
///
/// `parents` must be acyclic, which holds because a capability's parent
/// always exists before it does.
pub fn descendants<const N: usize>(parents: &[Option<u16>; N], root: usize) -> [bool; N] {
let mut marked = [false; N];
let Some(r) = marked.get_mut(root) else {
return marked;
};
*r = true;
// Each pass extends the marked set by at least one generation of
// children; a chain in an N-node forest is at most N - 1 long.
for _ in 0..N {
let mut changed = false;
for i in 0..N {
let parent_marked = parents
.get(i)
.copied()
.flatten()
.and_then(|p| marked.get(usize::from(p)).copied())
.unwrap_or(false);
if let Some(m) = marked.get_mut(i)
&& !*m
&& parent_marked
{
*m = true;
changed = true;
}
}
if !changed {
break;
}
}
marked
}
#[cfg(kani)]
mod proofs {
use super::descendants;
const N: usize = 4;
fn is_ancestor(parents: &[Option<u16>; N], root: usize, mut i: usize) -> bool {
for _ in 0..=N {
if i == root {
return true;
}
match parents[i].map(usize::from) {
Some(p) if p < N => i = p,
_ => return false,
}
}
false
}
/// On every acyclic forest of `N` capabilities, `descendants` marks
/// exactly the nodes `root` is an ancestor of: revocation removes the
/// whole subtree and nothing else.
#[kani::proof]
#[kani::unwind(6)]
fn marks_exactly_the_subtree() {
let mut parents = [None; N];
for slot in parents.iter_mut() {
if kani::any() {
*slot = Some(kani::any::<u16>() % N as u16);
}
}
// Only forests: following parents never revisits a node.
for start in 0..N {
let mut i = start;
let mut steps = 0;
while let Some(p) = parents[i] {
steps += 1;
kani::assume(steps <= N - 1);
i = p as usize;
}
}
let root = kani::any::<usize>() % N;
let marked = descendants(&parents, root);
for i in 0..N {
assert!(marked[i] == is_ancestor(&parents, root, i));
}
}
}

View File

@@ -0,0 +1,40 @@
use super::derivation::descendants;
#[test]
fn a_root_with_no_children_marks_only_itself() {
let parents = [None, None, None];
assert_eq!(descendants(&parents, 1), [false, true, false]);
}
#[test]
fn marks_children_and_grandchildren() {
// 0 -> 1 -> 2, 0 -> 3, 4 unrelated
let parents = [None, Some(0), Some(1), Some(0), None];
assert_eq!(descendants(&parents, 0), [true, true, true, true, false]);
}
#[test]
fn revoking_a_middle_node_spares_its_ancestors_and_siblings() {
// 0 -> 1 -> 2, 0 -> 3
let parents = [None, Some(0), Some(1), Some(0)];
assert_eq!(descendants(&parents, 1), [false, true, true, false]);
}
#[test]
fn deep_chains_are_followed_to_the_end() {
let parents = [None, Some(0), Some(1), Some(2), Some(3), Some(4)];
assert_eq!(descendants(&parents, 0), [true; 6]);
}
#[test]
fn chains_listed_out_of_order_are_still_found() {
// 3 -> 2 -> 1 -> 0 (children appear before their parents)
let parents = [Some(1), Some(2), Some(3), None];
assert_eq!(descendants(&parents, 3), [true, true, true, true]);
}
#[test]
fn an_out_of_range_root_marks_nothing() {
let parents = [None, Some(0)];
assert_eq!(descendants(&parents, 7), [false, false]);
}

View File

@@ -1,34 +1,80 @@
use types::{bounded::CapSlot, generation::Generation, permission::PermissionSet}; use types::{
badge::Badge, bounded::CapSlot, generation::Generation, id::ProcessId,
permission::PermissionSet,
};
pub mod authority; pub mod authority;
pub mod derivation;
use crate::{error::KernelError, resource::Resource}; #[cfg(test)]
#[path = "derivation_tests.rs"]
mod derivation_tests;
use crate::{error::KernelError, generations::SlotGenerations, resource::Resource};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
mod tests; mod tests;
const MAX_CAPS_PER_PROCESS: usize = 16; // Sized so that every representable `CapSlot` indexes a real slot.
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
/// Names one capability slot at one point in time. Both generations must
/// still match for the reference to resolve, so a reused pid or slot is
/// never mistaken for the capability that used to live there.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapRef {
pub process: ProcessId,
pub process_generation: Generation,
pub slot: CapSlot,
pub slot_generation: Generation,
}
#[derive(Clone)]
pub struct Capability { pub struct Capability {
resource: Resource, resource: Resource,
permissions: PermissionSet, permissions: PermissionSet,
generation: Generation, generation: Generation,
badge: Badge,
/// The capability this one was derived from (Mint, Delegate, IPC
/// transfer); `None` for capabilities the kernel created directly.
parent: Option<CapRef>,
} }
impl Capability { impl Capability {
pub fn new( pub fn new(resource: Resource, permissions: PermissionSet, generation: Generation) -> Self {
resource: Resource,
permissions: PermissionSet,
generation: Generation,
) -> Self {
Self { Self {
resource, resource,
permissions, permissions,
generation, generation,
badge: Badge::NONE,
parent: None,
} }
} }
pub fn with_parent(mut self, parent: Option<CapRef>) -> Self {
self.parent = parent;
self
}
pub fn parent(&self) -> Option<CapRef> {
self.parent
}
pub fn set_parent(&mut self, parent: Option<CapRef>) {
self.parent = parent;
}
/// The same capability with `badge` stamped on it.
pub fn with_badge(mut self, badge: Badge) -> Self {
self.badge = badge;
self
}
pub fn badge(&self) -> Badge {
self.badge
}
pub fn resource(&self) -> &Resource { pub fn resource(&self) -> &Resource {
&self.resource &self.resource
} }
@@ -44,39 +90,71 @@ impl Capability {
pub struct CapabilityTable { pub struct CapabilityTable {
slots: [Option<Capability>; MAX_CAPS_PER_PROCESS], slots: [Option<Capability>; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations<MAX_CAPS_PER_PROCESS>,
} }
impl CapabilityTable { impl CapabilityTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
slots: [const { None }; MAX_CAPS_PER_PROCESS], slots: [const { None }; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations::new(),
} }
} }
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> { fn slot(&self, slot: CapSlot) -> Option<&Capability> {
let idx = slot.value() as usize; self.slots.get(slot.value() as usize)?.as_ref()
self.slots[idx] }
.as_ref()
/// Generation of an occupied slot.
pub fn slot_generation(&self, slot: CapSlot) -> Option<Generation> {
self.slot(slot)?;
self.generations.current(slot.value() as usize)
}
pub fn get_mut(&mut self, slot: CapSlot) -> Result<&mut Capability, KernelError> {
self.slots
.get_mut(slot.value() as usize)
.and_then(Option::as_mut)
.ok_or(KernelError::InvalidSlot(slot)) .ok_or(KernelError::InvalidSlot(slot))
} }
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> { pub fn iter(&self) -> impl Iterator<Item = (CapSlot, &Capability)> {
let idx = self self.slots.iter().enumerate().filter_map(|(i, c)| {
.slots let cap = c.as_ref()?;
.iter() Some((CapSlot::new(i as u8)?, cap))
.position(|s| s.is_none()) })
.ok_or(KernelError::NoFreeSlot)?; }
self.slots[idx] = Some(cap); pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
CapSlot::new(idx as u8).ok_or(KernelError::NoFreeSlot) self.slot(slot).ok_or(KernelError::InvalidSlot(slot))
}
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> {
let generations = &self.generations;
let (idx, free) = self
.slots
.iter_mut()
.enumerate()
.find(|(i, s)| s.is_none() && generations.is_usable(*i))
.ok_or(KernelError::NoFreeSlot)?;
let slot = CapSlot::new(idx as u8).ok_or(KernelError::NoFreeSlot)?;
*free = Some(cap);
Ok(slot)
} }
pub fn remove(&mut self, slot: CapSlot) -> Result<(), KernelError> { pub fn remove(&mut self, slot: CapSlot) -> Result<(), KernelError> {
let idx = slot.value() as usize; let idx = slot.value() as usize;
if self.slots[idx].is_none() { let taken = self.slots.get_mut(idx).and_then(Option::take);
if taken.is_none() {
return Err(KernelError::InvalidSlot(slot)); return Err(KernelError::InvalidSlot(slot));
} }
self.slots[idx] = None; self.generations.advance(idx);
Ok(()) Ok(())
} }
} }
impl Default for CapabilityTable {
fn default() -> Self {
Self::new()
}
}

View File

@@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() {
#[test] #[test]
fn generation_mismatch_detected() { fn generation_mismatch_detected() {
let gen_v1 = Generation::initial(); let gen_v1 = Generation::initial();
let gen_v2 = gen_v1.next(); let gen_v2 = gen_v1.next().unwrap();
assert!(gen_v1.is_valid_against(&gen_v1)); assert!(gen_v1.is_valid_against(&gen_v1));
assert!(!gen_v1.is_valid_against(&gen_v2)); assert!(!gen_v1.is_valid_against(&gen_v2));
@@ -109,7 +109,6 @@ fn restrict_to_cannot_add_permissions() {
Permission::Write, Permission::Write,
Permission::Execute, Permission::Execute,
Permission::Delegate, Permission::Delegate,
Permission::Revoke,
]); ]);
let result = read_only.restrict_to(&wants_everything); let result = read_only.restrict_to(&wants_everything);
@@ -118,7 +117,6 @@ fn restrict_to_cannot_add_permissions() {
assert!(!result.allows(Permission::Write)); assert!(!result.allows(Permission::Write));
assert!(!result.allows(Permission::Execute)); assert!(!result.allows(Permission::Execute));
assert!(!result.allows(Permission::Delegate)); assert!(!result.allows(Permission::Delegate));
assert!(!result.allows(Permission::Revoke));
} }
#[test] #[test]
@@ -138,7 +136,6 @@ fn empty_permission_set_allows_nothing() {
assert!(!empty.allows(Permission::Write)); assert!(!empty.allows(Permission::Write));
assert!(!empty.allows(Permission::Execute)); assert!(!empty.allows(Permission::Execute));
assert!(!empty.allows(Permission::Delegate)); assert!(!empty.allows(Permission::Delegate));
assert!(!empty.allows(Permission::Revoke));
} }
#[test] #[test]
@@ -150,3 +147,45 @@ fn capability_preserves_resource_and_permissions() {
assert_eq!(cap.permissions(), &expected_permissions); assert_eq!(cap.permissions(), &expected_permissions);
assert_eq!(cap.generation(), &Generation::initial()); assert_eq!(cap.generation(), &Generation::initial());
} }
#[test]
fn removing_a_cap_changes_its_slot_generation() {
let mut table = CapabilityTable::new();
let slot = table.insert(endpoint_cap(1, &[Permission::Read])).unwrap();
let before = table.slot_generation(slot).unwrap();
table.remove(slot).unwrap();
assert_eq!(
table.slot_generation(slot),
None,
"empty slots have no generation"
);
let again = table.insert(endpoint_cap(2, &[Permission::Read])).unwrap();
assert_eq!(again, slot);
assert!(!before.is_valid_against(&table.slot_generation(again).unwrap()));
}
#[test]
fn iter_lists_occupied_slots() {
let mut table = CapabilityTable::new();
let a = table.insert(endpoint_cap(1, &[])).unwrap();
let b = table.insert(endpoint_cap(2, &[])).unwrap();
table.remove(a).unwrap();
let slots: [Option<types::bounded::CapSlot>; 2] = {
let mut it = table.iter().map(|(s, _)| s);
[it.next(), it.next()]
};
assert_eq!(slots, [Some(b), None]);
}
#[test]
fn caps_record_their_parent() {
let parent = CapRef {
process: types::id::ProcessId::from_raw(3),
process_generation: Generation::initial(),
slot: types::bounded::CapSlot::new(4).unwrap(),
slot_generation: Generation::initial(),
};
let cap = endpoint_cap(1, &[]).with_parent(Some(parent));
assert_eq!(cap.parent(), Some(parent));
assert_eq!(endpoint_cap(1, &[]).parent(), None);
}

View File

@@ -4,7 +4,10 @@ use types::{
id::{EndpointId, ProcessId, ThreadId}, id::{EndpointId, ProcessId, ThreadId},
}; };
use crate::{endpoint::wait_queue::WaitQueue, error::KernelError, message::Message}; use crate::{
endpoint::wait_queue::WaitQueue, error::KernelError, generations::SlotGenerations,
message::Message,
};
pub mod wait_queue; pub mod wait_queue;
@@ -14,18 +17,19 @@ mod tests;
pub struct Endpoint { pub struct Endpoint {
id: EndpointId, id: EndpointId,
/// The process that created it; the endpoint dies with its owner.
owner: ProcessId,
waiting_senders: WaitQueue, waiting_senders: WaitQueue,
waiting_receivers: WaitQueue, waiting_receivers: WaitQueue,
generation: Generation,
} }
impl Endpoint { impl Endpoint {
pub fn new(id: EndpointId) -> Self { pub fn new(id: EndpointId, owner: ProcessId) -> Self {
Self { Self {
id, id,
owner,
waiting_senders: WaitQueue::new(), waiting_senders: WaitQueue::new(),
waiting_receivers: WaitQueue::new(), waiting_receivers: WaitQueue::new(),
generation: Generation::initial(),
} }
} }
@@ -33,8 +37,8 @@ impl Endpoint {
&self.id &self.id
} }
pub fn generation(&self) -> &Generation { pub fn owner(&self) -> ProcessId {
&self.generation self.owner
} }
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> { pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
@@ -53,6 +57,22 @@ impl Endpoint {
self.waiting_receivers.pop() self.waiting_receivers.pop()
} }
pub fn senders(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.waiting_senders.iter()
}
pub fn receivers(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.waiting_receivers.iter()
}
pub fn is_sender(&self, thread: ThreadId) -> bool {
self.waiting_senders.contains(thread)
}
pub fn is_receiver(&self, thread: ThreadId) -> bool {
self.waiting_receivers.contains(thread)
}
pub fn has_waiting_sender(&self) -> bool { pub fn has_waiting_sender(&self) -> bool {
!self.waiting_senders.is_empty() !self.waiting_senders.is_empty()
} }
@@ -68,54 +88,87 @@ impl Endpoint {
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool { pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
self.waiting_receivers.remove_thread(thread) self.waiting_receivers.remove_thread(thread)
} }
pub fn revoke(&mut self) {
self.generation = self.generation.next();
}
} }
const MAX_ENDPOINTS: usize = 256; pub const MAX_ENDPOINTS: usize = if cfg!(kani) { 2 } else { 256 };
pub struct EndpointTable { pub struct EndpointTable {
endpoints: [Option<Endpoint>; MAX_ENDPOINTS], endpoints: [Option<Endpoint>; MAX_ENDPOINTS],
generations: SlotGenerations<MAX_ENDPOINTS>,
} }
impl EndpointTable { impl EndpointTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
endpoints: [const { None }; MAX_ENDPOINTS], endpoints: [const { None }; MAX_ENDPOINTS],
generations: SlotGenerations::new(),
} }
} }
pub fn create(&mut self) -> Option<EndpointId> { pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
let idx = self.endpoints.iter().position(|s| s.is_none())?; let generations = &self.generations;
let (idx, slot) = self
.endpoints
.iter_mut()
.enumerate()
.find(|(i, s)| s.is_none() && generations.is_usable(*i))?;
let id = EndpointId::from_raw(idx as u16); let id = EndpointId::from_raw(idx as u16);
self.endpoints[idx] = Some(Endpoint::new(id)); *slot = Some(Endpoint::new(id, owner));
Some(id) Some(id)
} }
/// Generation capabilities to a live endpoint must carry.
pub fn generation(&self, id: EndpointId) -> Option<Generation> {
self.get(id)?;
self.generations.current(id.raw() as usize)
}
pub fn iter(&self) -> impl Iterator<Item = &Endpoint> {
self.endpoints.iter().flatten()
}
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = EndpointId> + '_ {
self.endpoints
.iter()
.flatten()
.filter(move |ep| ep.owner == owner)
.map(|ep| ep.id)
}
pub fn get(&self, id: EndpointId) -> Option<&Endpoint> { pub fn get(&self, id: EndpointId) -> Option<&Endpoint> {
self.endpoints.get(id.raw() as usize).and_then(|s| s.as_ref()) self.endpoints
.get(id.raw() as usize)
.and_then(|s| s.as_ref())
} }
pub fn get_mut(&mut self, id: EndpointId) -> Option<&mut Endpoint> { pub fn get_mut(&mut self, id: EndpointId) -> Option<&mut Endpoint> {
self.endpoints.get_mut(id.raw() as usize).and_then(|s| s.as_mut()) self.endpoints
.get_mut(id.raw() as usize)
.and_then(|s| s.as_mut())
} }
/// Removes the endpoint; capabilities to it become invalid even if the
/// slot is later reused.
pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> { pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> {
self.endpoints.get_mut(id.raw() as usize).and_then(|s| s.take()) let ep = self.endpoints.get_mut(id.raw() as usize)?.take()?;
self.generations.advance(id.raw() as usize);
Some(ep)
} }
pub fn remove_thread_from_all(&mut self, thread: ThreadId) { pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
for slot in &mut self.endpoints { for ep in self.endpoints.iter_mut().flatten() {
if let Some(ep) = slot { ep.remove_sender(thread);
ep.remove_sender(thread); ep.remove_receiver(thread);
ep.remove_receiver(thread);
}
} }
} }
} }
impl Default for EndpointTable {
fn default() -> Self {
Self::new()
}
}
pub trait MessagePassing { pub trait MessagePassing {
fn send( fn send(
&mut self, &mut self,

View File

@@ -1,35 +1,39 @@
use crate::endpoint::wait_queue::MAX_WAITERS; use crate::endpoint::wait_queue::MAX_WAITERS;
use super::*; use super::*;
use types::id::{EndpointId, ThreadId}; use types::id::{EndpointId, ProcessId, ThreadId};
fn thread(id: u16) -> ThreadId { fn thread(id: u16) -> ThreadId {
ThreadId::from_raw(id) ThreadId::from_raw(id)
} }
fn owner() -> ProcessId {
ProcessId::from_raw(0)
}
fn test_endpoint() -> Endpoint { fn test_endpoint() -> Endpoint {
Endpoint::new(EndpointId::from_raw(1)) Endpoint::new(EndpointId::from_raw(1), owner())
} }
// --- WaitQueue tests --- // --- WaitQueue tests ---
#[test] #[test]
fn empty_queue_pop_returns_none() { fn empty_queue_pop_returns_none() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
assert!(queue.pop().is_none()); assert!(queue.pop().is_none());
assert!(queue.is_empty()); assert!(queue.is_empty());
} }
#[test] #[test]
fn push_then_pop_returns_same_thread() { fn push_then_pop_returns_same_thread() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
assert_eq!(queue.pop(), Some(thread(1))); assert_eq!(queue.pop(), Some(thread(1)));
} }
#[test] #[test]
fn pop_order_is_fifo() { fn pop_order_is_fifo() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap(); queue.push(thread(2)).unwrap();
queue.push(thread(3)).unwrap(); queue.push(thread(3)).unwrap();
@@ -42,7 +46,7 @@ fn pop_order_is_fifo() {
#[test] #[test]
fn full_queue_rejects_push() { fn full_queue_rejects_push() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
for i in 0..MAX_WAITERS { for i in 0..MAX_WAITERS {
assert!(queue.push(thread(i as u16)).is_some()); assert!(queue.push(thread(i as u16)).is_some());
} }
@@ -51,7 +55,7 @@ fn full_queue_rejects_push() {
#[test] #[test]
fn is_empty_after_drain() { fn is_empty_after_drain() {
let mut queue = WaitQueue::new(); let mut queue: WaitQueue = WaitQueue::new();
queue.push(thread(1)).unwrap(); queue.push(thread(1)).unwrap();
queue.push(thread(2)).unwrap(); queue.push(thread(2)).unwrap();
queue.pop(); queue.pop();
@@ -99,13 +103,34 @@ fn senders_and_receivers_are_independent() {
} }
#[test] #[test]
fn revoke_bumps_generation() { fn reused_slot_gets_a_new_generation() {
let mut ep = test_endpoint(); let mut table = EndpointTable::new();
let gen_before = *ep.generation(); let id = table.create(owner()).unwrap();
ep.revoke(); let old = table.generation(id).unwrap();
let gen_after = *ep.generation(); table.destroy(id);
assert_eq!(
table.generation(id),
None,
"dead endpoints have no generation"
);
let again = table.create(owner()).unwrap();
assert_eq!(again, id);
assert!(!old.is_valid_against(&table.generation(again).unwrap()));
}
assert!(!gen_before.is_valid_against(&gen_after)); #[test]
fn ids_owned_by_lists_only_that_owners_endpoints() {
let mut table = EndpointTable::new();
let a = table.create(owner()).unwrap();
table.create(ProcessId::from_raw(1)).unwrap();
let c = table.create(owner()).unwrap();
let mut ids = [EndpointId::from_raw(u16::MAX); 4];
let mut n = 0;
for id in table.ids_owned_by(owner()) {
ids[n] = id;
n += 1;
}
assert_eq!(&ids[..n], &[a, c]);
} }
#[test] #[test]
@@ -141,22 +166,22 @@ fn rendezvous_receiver_first() {
#[test] #[test]
fn table_create_and_lookup() { fn table_create_and_lookup() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
assert!(table.get(id).is_some()); assert!(table.get(id).is_some());
} }
#[test] #[test]
fn table_create_returns_unique_ids() { fn table_create_returns_unique_ids() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let a = table.create().unwrap(); let a = table.create(owner()).unwrap();
let b = table.create().unwrap(); let b = table.create(owner()).unwrap();
assert_ne!(a, b); assert_ne!(a, b);
} }
#[test] #[test]
fn table_destroy_then_get_fails() { fn table_destroy_then_get_fails() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
let removed = table.destroy(id); let removed = table.destroy(id);
assert!(removed.is_some()); assert!(removed.is_some());
assert!(table.get(id).is_none()); assert!(table.get(id).is_none());
@@ -165,16 +190,57 @@ fn table_destroy_then_get_fails() {
#[test] #[test]
fn table_get_mut_allows_enqueue() { fn table_get_mut_allows_enqueue() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table.get_mut(id).unwrap().enqueue_sender(thread(1)).unwrap(); table
.get_mut(id)
.unwrap()
.enqueue_sender(thread(1))
.unwrap();
assert!(table.get(id).unwrap().has_waiting_sender()); assert!(table.get(id).unwrap().has_waiting_sender());
} }
#[test] #[test]
fn table_destroy_reuses_slot() { fn table_destroy_reuses_slot() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table.destroy(id); table.destroy(id);
let new_id = table.create().unwrap(); let new_id = table.create(owner()).unwrap();
assert_eq!(id, new_id); assert_eq!(id, new_id);
} }
#[test]
fn wait_queue_is_fifo_even_after_a_pop_leaves_a_hole() {
let mut q: WaitQueue = WaitQueue::new();
q.push(thread(1)).unwrap();
q.push(thread(2)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
q.push(thread(3)).unwrap();
assert_eq!(q.pop(), Some(thread(2)), "thread 3 jumped the queue");
assert_eq!(q.pop(), Some(thread(3)));
}
#[test]
fn wait_queue_keeps_order_around_a_removed_thread() {
let mut q: WaitQueue = WaitQueue::new();
for n in 1..=3 {
q.push(thread(n)).unwrap();
}
assert!(q.remove_thread(thread(2)));
q.push(thread(4)).unwrap();
assert_eq!(q.pop(), Some(thread(1)));
assert_eq!(q.pop(), Some(thread(3)));
assert_eq!(q.pop(), Some(thread(4)));
assert_eq!(q.pop(), None);
}
#[test]
fn wait_queue_kani_trace_push_push_remove() {
let mut q: WaitQueue<3> = WaitQueue::new();
q.push(thread(3)).unwrap();
q.push(thread(2)).unwrap();
assert!(q.remove_thread(thread(3)));
assert!(!q.remove_thread(thread(0)));
assert!(!q.remove_thread(thread(3)));
assert_eq!(q.pop(), Some(thread(2)));
assert_eq!(q.pop(), None);
}

View File

@@ -1,46 +1,141 @@
use types::id::ThreadId; use types::id::ThreadId;
pub const MAX_WAITERS: usize = 16; pub const MAX_WAITERS: usize = if cfg!(kani) { 2 } else { 16 };
pub struct WaitQueue { /// Bounded FIFO of blocked threads. Order is preserved across pops and
entries: [Option<ThreadId>; MAX_WAITERS], /// removals, so no waiter can be overtaken.
count: u8, pub struct WaitQueue<const N: usize = MAX_WAITERS> {
entries: [Option<ThreadId>; N],
len: usize,
} }
impl WaitQueue { impl<const N: usize> WaitQueue<N> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
entries: [None; MAX_WAITERS], entries: [None; N],
count: 0, len: 0,
} }
} }
pub fn push(&mut self, thread: ThreadId) -> Option<()> { pub fn push(&mut self, thread: ThreadId) -> Option<()> {
let slot = self.entries.iter_mut().find(|s| s.is_none())?; if self.len == N || self.contains(thread) {
*slot = Some(thread); return None;
self.count += 1; }
*self.entries.get_mut(self.len)? = Some(thread);
self.len += 1;
Some(()) Some(())
} }
pub fn pop(&mut self) -> Option<ThreadId> { pub fn pop(&mut self) -> Option<ThreadId> {
let slot = self.entries.iter_mut().find(|s| s.is_some())?; let head = self.entries.first().copied().flatten()?;
let thread = slot.take(); self.close_gap(0);
self.count -= 1; Some(head)
thread
} }
pub fn remove_thread(&mut self, thread: ThreadId) -> bool { pub fn remove_thread(&mut self, thread: ThreadId) -> bool {
for slot in &mut self.entries { match self.entries[..self.len]
if *slot == Some(thread) { .iter()
*slot = None; .position(|e| *e == Some(thread))
self.count -= 1; {
return true; Some(i) => {
self.close_gap(i);
true
} }
None => false,
} }
false }
pub fn iter(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.entries.iter().take(self.len).flatten().copied()
}
pub fn contains(&self, thread: ThreadId) -> bool {
self.entries
.iter()
.take(self.len)
.any(|e| *e == Some(thread))
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.count == 0 self.len == 0
}
/// Removes entry `i`, shifting later entries forward to keep order.
fn close_gap(&mut self, i: usize) {
for j in i..self.len.saturating_sub(1) {
let next = self.entries.get(j + 1).copied().flatten();
if let Some(e) = self.entries.get_mut(j) {
*e = next;
}
}
self.len = self.len.saturating_sub(1);
if let Some(last) = self.entries.get_mut(self.len) {
*last = None;
}
}
}
impl<const N: usize> Default for WaitQueue<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(kani)]
mod proofs {
use super::*;
/// Against a simple reference model (a plain array kept in arrival
/// order), any sequence of pushes, pops and removals behaves the same:
/// FIFO order, no duplicates, nothing lost.
#[kani::proof]
#[kani::unwind(7)]
fn behaves_like_a_fifo_without_duplicates() {
const CAP: usize = 3;
let mut q: WaitQueue<CAP> = WaitQueue::new();
let mut model = [0u16; CAP];
let mut len = 0usize;
for _ in 0..5 {
let t: u16 = kani::any::<u16>() % 4;
match kani::any::<u8>() % 3 {
0 => {
let ok = q.push(ThreadId::from_raw(t)).is_some();
let dup = model[..len].contains(&t);
assert!(ok == (!dup && len < CAP));
if ok {
model[len] = t;
len += 1;
}
}
1 => {
let got = q.pop();
if len == 0 {
assert!(got.is_none());
} else {
assert!(got == Some(ThreadId::from_raw(model[0])));
for j in 0..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
}
_ => {
let removed = q.remove_thread(ThreadId::from_raw(t));
match model[..len].iter().position(|&m| m == t) {
Some(i) => {
assert!(removed);
for j in i..len - 1 {
model[j] = model[j + 1];
}
len -= 1;
}
None => assert!(!removed),
}
}
}
assert!(q.len == len);
assert!(q.is_empty() == (len == 0));
}
} }
} }

View File

@@ -8,7 +8,10 @@ use types::{
pub enum KernelError { pub enum KernelError {
InvalidSlot(CapSlot), InvalidSlot(CapSlot),
RevokedCapability(CapSlot), RevokedCapability(CapSlot),
PermissionDenied { slot: CapSlot, needed: Permission }, PermissionDenied {
slot: CapSlot,
needed: Permission,
},
NoFreeSlot, NoFreeSlot,
InvalidProcess(ProcessId), InvalidProcess(ProcessId),
WouldBlock, WouldBlock,
@@ -23,4 +26,19 @@ pub enum KernelError {
EndpointFull, EndpointFull,
ProcessCreateDenied, ProcessCreateDenied,
IrqAlreadyOwned(IrqId), IrqAlreadyOwned(IrqId),
/// Address outside the user window, unaligned, or not owned by the caller.
InvalidAddress,
/// The target virtual range already holds a mapping.
AddressInUse,
InvalidRegion,
/// The region is mapped executable somewhere and the request would map
/// it writable, or the other way round (W^X across processes).
WritableAndExecutable,
/// The object a capability would refer to no longer exists.
NoSuchObject,
/// The capability names the wrong kind of object for this operation.
WrongObject,
/// `Reply` with an empty reply slot, or a call whose reply was dropped.
NoReply,
InvalidArgument,
} }

View File

@@ -0,0 +1,86 @@
//! Per-slot generation counters shared by the kernel's object tables.
use types::generation::Generation;
#[cfg(test)]
#[path = "generations_tests.rs"]
mod tests;
/// A slot's generation changes every time the object in it is destroyed
/// or revoked, so capabilities minted earlier stop matching. A slot whose
/// counter runs out is retired for good rather than reused.
pub struct SlotGenerations<const N: usize> {
gens: [Option<Generation>; N],
}
impl<const N: usize> SlotGenerations<N> {
pub fn new() -> Self {
Self {
gens: [Some(Generation::initial()); N],
}
}
/// Current generation of `idx`, `None` if retired or out of range.
pub fn current(&self, idx: usize) -> Option<Generation> {
*self.gens.get(idx)?
}
pub fn is_usable(&self, idx: usize) -> bool {
self.current(idx).is_some()
}
/// Invalidates every capability minted for the current generation.
pub fn advance(&mut self, idx: usize) {
if let Some(g) = self.gens.get_mut(idx) {
*g = g.and_then(|g| g.next());
}
}
}
impl<const N: usize> Default for SlotGenerations<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(kani)]
mod proofs {
use super::SlotGenerations;
use types::generation::Generation;
/// However many times a slot is advanced, a generation observed earlier
/// never comes back, so a capability minted for it stays dead.
#[kani::proof]
#[kani::unwind(5)]
fn an_observed_generation_never_returns() {
let mut g: SlotGenerations<1> = SlotGenerations::new();
g.gens[0] = if kani::any() {
Some(Generation::from_raw(kani::any()))
} else {
None
};
let observed = g.current(0);
let steps: u8 = kani::any();
kani::assume(steps >= 1 && steps <= 4);
for _ in 0..steps {
g.advance(0);
if let (Some(old), Some(now)) = (observed, g.current(0)) {
assert!(!old.is_valid_against(&now));
}
}
}
/// A retired slot is never usable again.
#[kani::proof]
#[kani::unwind(5)]
fn retired_slots_stay_retired() {
let mut g: SlotGenerations<1> = SlotGenerations::new();
g.gens[0] = None;
let steps: u8 = kani::any();
kani::assume(steps <= 4);
for _ in 0..steps {
g.advance(0);
}
assert!(!g.is_usable(0));
}
}

View File

@@ -0,0 +1,36 @@
use super::SlotGenerations;
use types::generation::Generation;
#[test]
fn slots_start_at_the_initial_generation() {
let g: SlotGenerations<2> = SlotGenerations::new();
assert_eq!(g.current(0), Some(Generation::initial()));
assert!(g.is_usable(1));
}
#[test]
fn advance_changes_the_generation() {
let mut g: SlotGenerations<2> = SlotGenerations::new();
let before = g.current(0);
g.advance(0);
assert_ne!(g.current(0), before);
assert_eq!(g.current(1), before, "other slots untouched");
}
#[test]
fn out_of_range_slots_are_never_usable() {
let mut g: SlotGenerations<2> = SlotGenerations::new();
assert_eq!(g.current(2), None);
g.advance(2); // no panic
}
#[test]
fn exhausted_counter_retires_the_slot() {
let mut g: SlotGenerations<1> = SlotGenerations::new();
g.gens[0] = Some(Generation::from_raw(u32::MAX));
g.advance(0);
assert_eq!(g.current(0), None);
assert!(!g.is_usable(0));
g.advance(0);
assert_eq!(g.current(0), None, "retired slots stay retired");
}

View File

@@ -1,38 +1,66 @@
use types::id::{IrqId, ThreadId}; use types::{
badge::Badge,
use crate::error::KernelError; generation::Generation,
id::{IrqId, NotificationId},
};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
mod tests; mod tests;
const MAX_IRQS: usize = 256; const MAX_IRQS: usize = if cfg!(kani) { 2 } else { 256 };
/// Where an interrupt is delivered: a signal on a notification, through a
/// capability snapshot (generation and badge) taken when the route was set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IrqRoute {
pub notification: NotificationId,
pub generation: Generation,
pub badge: Badge,
}
pub struct IrqRoutingTable { pub struct IrqRoutingTable {
owners: [Option<ThreadId>; MAX_IRQS], routes: [Option<IrqRoute>; MAX_IRQS],
} }
impl IrqRoutingTable { impl IrqRoutingTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
owners: [None; MAX_IRQS], routes: [None; MAX_IRQS],
} }
} }
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> { /// Whether `irq` is a line this table can route.
let idx = irq.raw() as usize; pub fn is_valid(&self, irq: IrqId) -> bool {
if self.owners[idx].is_some() { (irq.raw() as usize) < MAX_IRQS
return Err(KernelError::IrqAlreadyOwned(irq)); }
/// Routes `irq`, replacing any previous route. Fails for lines beyond
/// the table.
pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool {
match self.routes.get_mut(irq.raw() as usize) {
Some(slot) => {
*slot = Some(route);
true
}
None => false,
} }
self.owners[idx] = Some(thread);
Ok(())
} }
pub fn unregister(&mut self, irq: IrqId) { pub fn clear(&mut self, irq: IrqId) {
self.owners[irq.raw() as usize] = None; if let Some(slot) = self.routes.get_mut(irq.raw() as usize) {
*slot = None;
}
} }
pub fn owner(&self, irq: IrqId) -> Option<ThreadId> { /// The route for `irq`; `None` for unknown or unrouted lines.
self.owners[irq.raw() as usize] pub fn route(&self, irq: IrqId) -> Option<IrqRoute> {
*self.routes.get(irq.raw() as usize)?
}
}
impl Default for IrqRoutingTable {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -1,50 +1,43 @@
use super::*; use super::*;
use types::id::{IrqId, ThreadId}; use types::badge::Badge;
use types::generation::Generation;
use types::id::{IrqId, NotificationId};
fn irq(id: u16) -> IrqId { fn route(n: u16) -> IrqRoute {
IrqId::from_raw(id) IrqRoute {
} notification: NotificationId::from_raw(n),
generation: Generation::initial(),
fn thread(id: u16) -> ThreadId { badge: Badge::new(1 << n),
ThreadId::from_raw(id) }
} }
#[test] #[test]
fn new_table_has_no_owners() { fn new_table_routes_nothing() {
let table = IrqRoutingTable::new(); let t = IrqRoutingTable::new();
assert!(table.owner(irq(0)).is_none()); assert_eq!(t.route(IrqId::from_raw(10)), None);
assert!(table.owner(irq(10)).is_none());
} }
#[test] #[test]
fn register_and_lookup() { fn set_then_route() {
let mut table = IrqRoutingTable::new(); let mut t = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap(); assert!(t.set(IrqId::from_raw(10), route(3)));
assert_eq!(table.owner(irq(5)), Some(thread(1))); assert_eq!(t.route(IrqId::from_raw(10)), Some(route(3)));
} }
#[test] #[test]
fn register_already_owned_fails() { fn set_replaces_and_clear_removes() {
let mut table = IrqRoutingTable::new(); let mut t = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap(); t.set(IrqId::from_raw(10), route(3));
assert!(table.register(irq(5), thread(2)).is_err()); t.set(IrqId::from_raw(10), route(4));
assert_eq!(t.route(IrqId::from_raw(10)), Some(route(4)));
t.clear(IrqId::from_raw(10));
assert_eq!(t.route(IrqId::from_raw(10)), None);
} }
#[test] #[test]
fn unregister_then_reregister() { fn lines_beyond_the_table_are_rejected_without_panicking() {
let mut table = IrqRoutingTable::new(); let mut t = IrqRoutingTable::new();
table.register(irq(5), thread(1)).unwrap(); assert!(!t.set(IrqId::from_raw(1000), route(1)));
table.unregister(irq(5)); assert_eq!(t.route(IrqId::from_raw(1000)), None);
assert!(table.owner(irq(5)).is_none()); t.clear(IrqId::from_raw(1000));
table.register(irq(5), thread(2)).unwrap();
assert_eq!(table.owner(irq(5)), Some(thread(2)));
}
#[test]
fn different_irqs_independent() {
let mut table = IrqRoutingTable::new();
table.register(irq(0), thread(10)).unwrap();
table.register(irq(1), thread(20)).unwrap();
assert_eq!(table.owner(irq(0)), Some(thread(10)));
assert_eq!(table.owner(irq(1)), Some(thread(20)));
} }

View File

@@ -1,4 +1,18 @@
#![no_std] #![no_std]
// A kernel panic takes the whole system down, so kernel code may not
// contain any way to panic. Tests and proofs are exempt.
#![cfg_attr(
not(any(test, kani)),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing
)
)]
#[cfg(kani)] #[cfg(kani)]
pub mod proofs; pub mod proofs;
@@ -7,9 +21,11 @@ pub mod capability;
pub mod endpoint; pub mod endpoint;
pub mod error; pub mod error;
pub mod fault; pub mod fault;
pub mod generations;
pub mod irq; pub mod irq;
pub mod memory; pub mod memory;
pub mod message; pub mod message;
pub mod notification;
pub mod process; pub mod process;
pub mod resource; pub mod resource;
pub mod scheduler; pub mod scheduler;

View File

@@ -0,0 +1,232 @@
//! Physical frame ownership. The kernel's single source of truth for what
//! every frame of the user pool is used for and who is charged for it.
use types::{
address::{PhysicalAddress, VirtualAddress},
id::{ProcessId, RegionId},
mapping::PagePerms,
};
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(kani)]
#[path = "proofs.rs"]
mod proofs;
pub const PAGE_SIZE: usize = 4096;
/// Frames the kernel can track. The board's user pool must fit.
pub const MAX_FRAMES: usize = if cfg!(kani) { 6 } else { 4096 };
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameUse {
/// Mapped exactly once, at `page` in `owner`'s address space.
User {
owner: ProcessId,
page: u32,
perms: PagePerms,
},
/// Holds part of `owner`'s page tables.
PageTable { owner: ProcessId },
/// Backs a shared memory region.
Region { region: RegionId },
}
impl FrameUse {
/// A user mapping at `va`, which must be page aligned.
pub fn user(owner: ProcessId, va: VirtualAddress, perms: PagePerms) -> Option<Self> {
if !va.raw().is_multiple_of(PAGE_SIZE) {
return None;
}
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
Some(Self::User { owner, page, perms })
}
}
pub struct FrameTable<const N: usize = MAX_FRAMES> {
base: PhysicalAddress,
total: usize,
frames: [Option<FrameUse>; N],
free: usize,
}
impl<const N: usize> FrameTable<N> {
pub fn new(base: PhysicalAddress, total: usize) -> Self {
let total = total.min(N);
Self {
base,
total,
frames: [None; N],
free: total,
}
}
pub fn base(&self) -> PhysicalAddress {
self.base
}
pub fn capacity(&self) -> usize {
self.total
}
pub fn free_count(&self) -> usize {
self.free
}
fn index_of(&self, pa: PhysicalAddress) -> Option<usize> {
let offset = pa.raw().checked_sub(self.base.raw())?;
if !offset.is_multiple_of(PAGE_SIZE) {
return None;
}
let idx = offset / PAGE_SIZE;
(idx < self.total).then_some(idx)
}
fn address_of(&self, idx: usize) -> PhysicalAddress {
self.base.offset(idx)
}
/// The frames that exist (`total` is clamped to `N` at construction).
fn live(&self) -> &[Option<FrameUse>] {
self.frames.get(..self.total).unwrap_or(&[])
}
fn live_mut(&mut self) -> &mut [Option<FrameUse>] {
self.frames.get_mut(..self.total).unwrap_or(&mut [])
}
pub fn get(&self, pa: PhysicalAddress) -> Option<&FrameUse> {
self.frames.get(self.index_of(pa)?)?.as_ref()
}
pub fn alloc(&mut self, usage: FrameUse) -> Option<PhysicalAddress> {
let (idx, slot) = self
.live_mut()
.iter_mut()
.enumerate()
.find(|(_, f)| f.is_none())?;
*slot = Some(usage);
self.free -= 1;
Some(self.address_of(idx))
}
/// Allocates `count` physically adjacent frames; frame `i` gets `usage(i)`.
pub fn alloc_contiguous(
&mut self,
count: usize,
usage: impl Fn(usize) -> FrameUse,
) -> Option<PhysicalAddress> {
if count == 0 {
return None;
}
let mut run = 0;
let mut start = None;
for (idx, f) in self.live().iter().enumerate() {
if f.is_some() {
run = 0;
continue;
}
run += 1;
if run == count {
start = Some(idx + 1 - count);
break;
}
}
let start = start?;
let run = self.live_mut().get_mut(start..start + count)?;
for (i, f) in run.iter_mut().enumerate() {
*f = Some(usage(i));
}
self.free -= count;
Some(self.address_of(start))
}
/// Frees an allocated frame and reports what it was used for.
pub fn free(&mut self, pa: PhysicalAddress) -> Option<FrameUse> {
let idx = self.index_of(pa)?;
let usage = self.frames.get_mut(idx)?.take()?;
self.free += 1;
Some(usage)
}
/// Changes the use of an allocated frame (e.g. moving it to another
/// process). Fails on free frames.
pub fn retag(&mut self, pa: PhysicalAddress, usage: FrameUse) -> bool {
match self
.index_of(pa)
.and_then(|i| self.frames.get_mut(i))
.and_then(Option::as_mut)
{
Some(slot) => {
*slot = usage;
true
}
None => false,
}
}
/// The frame mapped at `va` in `owner`, if `owner` holds it as a user page.
pub fn find_user(
&self,
owner: ProcessId,
va: VirtualAddress,
) -> Option<(PhysicalAddress, PagePerms)> {
if !va.raw().is_multiple_of(PAGE_SIZE) {
return None;
}
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
self.live().iter().enumerate().find_map(|(idx, f)| match f {
Some(FrameUse::User {
owner: o,
page: p,
perms,
}) if *o == owner && *p == page => Some((self.address_of(idx), *perms)),
_ => None,
})
}
/// Every allocated frame and its use.
pub fn iter(&self) -> impl Iterator<Item = (PhysicalAddress, &FrameUse)> + '_ {
self.live()
.iter()
.enumerate()
.filter_map(|(idx, f)| f.as_ref().map(|u| (self.address_of(idx), u)))
}
/// User pages of `owner` as (frame, virtual address) pairs.
pub fn user_frames_of(
&self,
owner: ProcessId,
) -> impl Iterator<Item = (PhysicalAddress, VirtualAddress)> + '_ {
self.live()
.iter()
.enumerate()
.filter_map(move |(idx, f)| match f {
Some(FrameUse::User { owner: o, page, .. }) if *o == owner => Some((
self.address_of(idx),
VirtualAddress::from_raw(*page as usize * PAGE_SIZE),
)),
_ => None,
})
}
/// Frees every user and page-table frame charged to `owner`; returns
/// how many were freed.
pub fn free_owned_by(&mut self, owner: ProcessId) -> usize {
let mut freed = 0;
for slot in self.live_mut().iter_mut() {
let owned = matches!(
slot,
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if *o == owner
);
if owned {
*slot = None;
freed += 1;
}
}
self.free += freed;
freed
}
}

View File

@@ -0,0 +1,195 @@
//! Bounded proofs over every reachable state of a small frame table.
//! Each harness starts from an arbitrary *well-formed* table (free count
//! matches the number of empty slots) and checks one operation.
use super::*;
use types::address::{PhysicalAddress, VirtualAddress};
use types::id::{ProcessId, RegionId};
use types::mapping::PagePerms;
const N: usize = 4;
const BASE: usize = 0x8400_0000;
fn any_perms() -> PagePerms {
match kani::any::<u8>() % 3 {
0 => PagePerms::ReadOnly,
1 => PagePerms::ReadWrite,
_ => PagePerms::ReadExecute,
}
}
fn any_use() -> FrameUse {
// Small id ranges keep owner collisions likely, which is where the
// interesting cases are.
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
match kani::any::<u8>() % 3 {
0 => FrameUse::User {
owner,
page: kani::any::<u32>() % 4,
perms: any_perms(),
},
1 => FrameUse::PageTable { owner },
_ => FrameUse::Region {
region: RegionId::from_raw(kani::any::<u16>() % 2),
},
}
}
fn any_slot() -> Option<FrameUse> {
if kani::any() { Some(any_use()) } else { None }
}
fn any_table() -> FrameTable<N> {
let frames = [any_slot(), any_slot(), any_slot(), any_slot()];
let free = frames.iter().filter(|f| f.is_none()).count();
FrameTable {
base: PhysicalAddress::from_raw(BASE),
total: N,
frames,
free,
}
}
fn well_formed(t: &FrameTable<N>) -> bool {
t.free == t.frames[..t.total].iter().filter(|f| f.is_none()).count()
}
fn any_pa() -> PhysicalAddress {
// Anything from below the pool to past its end, aligned or not.
PhysicalAddress::from_raw(BASE - 4096 + kani::any::<usize>() % ((N + 2) * 4096))
}
#[kani::proof]
#[kani::unwind(6)]
fn alloc_only_hands_out_free_frames() {
let mut t = any_table();
let before = t.frames;
let usage = any_use();
match t.alloc(usage) {
Some(pa) => {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(idx < N);
assert!(before[idx].is_none());
assert!(t.frames[idx] == Some(usage));
for i in 0..N {
if i != idx {
assert!(t.frames[i] == before[i]);
}
}
}
None => {
assert!(before.iter().all(Option::is_some));
assert!(t.frames == before);
}
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn free_releases_exactly_one_allocated_frame_or_nothing() {
let mut t = any_table();
let before = t.frames;
let pa = any_pa();
match t.free(pa) {
Some(usage) => {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(pa.raw() % PAGE_SIZE == 0);
assert!(before[idx] == Some(usage));
assert!(t.frames[idx].is_none());
for i in 0..N {
if i != idx {
assert!(t.frames[i] == before[i]);
}
}
}
None => assert!(t.frames == before),
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn retag_never_allocates() {
let mut t = any_table();
let before = t.frames;
let pa = any_pa();
let usage = any_use();
let changed = t.retag(pa, usage);
for i in 0..N {
if before[i].is_none() {
assert!(t.frames[i].is_none());
}
}
if !changed {
assert!(t.frames == before);
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn free_owned_by_spares_everyone_else() {
let mut t = any_table();
let before = t.frames;
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
t.free_owned_by(owner);
for i in 0..N {
let was_owned = matches!(
before[i],
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if o == owner
);
if was_owned {
assert!(t.frames[i].is_none());
} else {
assert!(t.frames[i] == before[i]);
}
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn find_user_only_reports_the_owners_mapping() {
let t = any_table();
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
let va = VirtualAddress::from_raw(kani::any::<usize>() % (5 * 4096));
if let Some((pa, perms)) = t.find_user(owner, va) {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(idx < N);
assert!(va.raw() % PAGE_SIZE == 0);
assert!(
t.frames[idx]
== Some(FrameUse::User {
owner,
page: (va.raw() / PAGE_SIZE) as u32,
perms,
})
);
}
}
#[kani::proof]
#[kani::unwind(6)]
fn alloc_contiguous_only_claims_a_free_run() {
let mut t = any_table();
let before = t.frames;
let count = kani::any::<usize>() % (N + 2);
let region = RegionId::from_raw(1);
match t.alloc_contiguous(count, |_| FrameUse::Region { region }) {
Some(pa) => {
let start = (pa.raw() - BASE) / PAGE_SIZE;
assert!(count > 0 && start + count <= N);
for i in 0..N {
if i >= start && i < start + count {
assert!(before[i].is_none());
assert!(t.frames[i] == Some(FrameUse::Region { region }));
} else {
assert!(t.frames[i] == before[i]);
}
}
}
None => assert!(t.frames == before),
}
assert!(well_formed(&t));
}

View File

@@ -0,0 +1,188 @@
use super::*;
use types::address::{PhysicalAddress, VirtualAddress};
use types::id::{ProcessId, RegionId};
use types::mapping::PagePerms;
const BASE: usize = 0x8400_0000;
fn table() -> FrameTable<8> {
FrameTable::new(PhysicalAddress::from_raw(BASE), 8)
}
fn pid(n: u16) -> ProcessId {
ProcessId::from_raw(n)
}
fn va(addr: usize) -> VirtualAddress {
VirtualAddress::from_raw(addr)
}
fn user(owner: u16, addr: usize) -> FrameUse {
FrameUse::user(pid(owner), va(addr), PagePerms::ReadWrite).unwrap()
}
#[test]
fn new_table_has_every_frame_free() {
let t = table();
assert_eq!(t.free_count(), 8);
assert_eq!(t.capacity(), 8);
}
#[test]
fn total_is_clamped_to_capacity() {
let t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 100);
assert_eq!(t.free_count(), 4);
}
#[test]
fn user_use_requires_page_aligned_va() {
assert!(FrameUse::user(pid(0), va(0x1234), PagePerms::ReadOnly).is_none());
assert!(FrameUse::user(pid(0), va(0x1000), PagePerms::ReadOnly).is_some());
}
#[test]
fn alloc_hands_out_distinct_frames_inside_the_pool() {
let mut t = table();
let a = t.alloc(user(0, 0x1000)).unwrap();
let b = t.alloc(user(0, 0x2000)).unwrap();
assert_ne!(a, b);
for pa in [a, b] {
assert!(pa.raw() >= BASE && pa.raw() < BASE + 8 * 4096);
assert_eq!(pa.raw() % 4096, 0);
}
assert_eq!(t.free_count(), 6);
}
#[test]
fn alloc_fails_when_exhausted() {
let mut t = table();
for i in 0..8 {
t.alloc(user(0, (i + 1) * 0x1000)).unwrap();
}
assert!(t.alloc(user(0, 0x9000)).is_none());
}
#[test]
fn free_returns_the_frame_and_its_use() {
let mut t = table();
let pa = t.alloc(user(3, 0x5000)).unwrap();
assert_eq!(t.free(pa), Some(user(3, 0x5000)));
assert_eq!(t.free_count(), 8);
}
#[test]
fn freeing_a_free_frame_is_rejected() {
let mut t = table();
let pa = t.alloc(user(0, 0x1000)).unwrap();
t.free(pa).unwrap();
assert_eq!(t.free(pa), None);
assert_eq!(t.free_count(), 8);
}
#[test]
fn freeing_outside_the_pool_or_unaligned_is_rejected() {
let mut t = table();
t.alloc(user(0, 0x1000)).unwrap();
assert_eq!(t.free(PhysicalAddress::from_raw(BASE - 4096)), None);
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 8 * 4096)), None);
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 1)), None);
assert_eq!(t.free_count(), 7);
}
#[test]
fn find_user_matches_owner_and_va() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
assert_eq!(
t.find_user(pid(1), va(0x3000)),
Some((pa, PagePerms::ReadWrite))
);
assert_eq!(t.find_user(pid(2), va(0x3000)), None);
assert_eq!(t.find_user(pid(1), va(0x4000)), None);
}
#[test]
fn retag_moves_a_frame_to_a_new_use() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
assert!(t.retag(pa, user(2, 0x7000)));
assert_eq!(t.find_user(pid(1), va(0x3000)), None);
assert_eq!(t.find_user(pid(2), va(0x7000)).map(|(p, _)| p), Some(pa));
}
#[test]
fn retag_of_a_free_frame_is_rejected() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
t.free(pa);
assert!(!t.retag(pa, user(2, 0x7000)));
assert_eq!(t.free_count(), 8);
}
#[test]
fn alloc_contiguous_returns_adjacent_frames() {
let mut t = table();
let base = t
.alloc_contiguous(3, |_| FrameUse::Region {
region: RegionId::from_raw(0),
})
.unwrap();
assert_eq!(t.free_count(), 5);
for i in 0..3 {
assert_eq!(
t.get(base.offset(i)),
Some(&FrameUse::Region {
region: RegionId::from_raw(0)
})
);
}
}
#[test]
fn alloc_contiguous_fails_without_a_long_enough_run() {
let mut t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 4);
let a = t.alloc(user(0, 0x1000)).unwrap();
let _b = t.alloc(user(0, 0x2000)).unwrap();
let _c = t.alloc(user(0, 0x3000)).unwrap();
t.free(a);
// free frames: 0 and 3 — no run of 2
assert!(
t.alloc_contiguous(2, |_| FrameUse::PageTable { owner: pid(0) })
.is_none()
);
assert_eq!(t.free_count(), 2);
}
#[test]
fn free_owned_by_releases_only_that_processes_frames() {
let mut t = table();
t.alloc(user(1, 0x1000)).unwrap();
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
let other = t.alloc(user(2, 0x1000)).unwrap();
let region = t
.alloc(FrameUse::Region {
region: RegionId::from_raw(1),
})
.unwrap();
assert_eq!(t.free_owned_by(pid(1)), 2);
assert_eq!(t.free_count(), 6);
assert!(t.get(other).is_some());
assert!(t.get(region).is_some());
}
#[test]
fn user_frames_of_lists_mappings_of_one_process() {
let mut t = table();
t.alloc(user(1, 0x1000)).unwrap();
t.alloc(user(2, 0x2000)).unwrap();
t.alloc(user(1, 0x3000)).unwrap();
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
let mut vas = [0usize; 4];
let mut n = 0;
for (_, v) in t.user_frames_of(pid(1)) {
vas[n] = v.raw();
n += 1;
}
assert_eq!(&vas[..n], &[0x1000, 0x3000]);
}

View File

@@ -1,118 +1,210 @@
use crate::{capability::CapRef, generations::SlotGenerations};
use types::{ use types::{
address::PhysicalAddress, address::{PhysicalAddress, VirtualAddress},
bounded::PageCount, bounded::PageCount,
generation::Generation, generation::Generation,
id::{ProcessId, RegionId}, id::{ProcessId, RegionId},
mapping::PagePerms,
}; };
pub mod page_pool; pub mod frame;
pub mod process_memory; pub mod quota;
#[cfg(test)]
#[path = "quota_tests.rs"]
mod quota_tests;
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
mod tests; mod tests;
const MAX_REGIONS: usize = 256; pub const MAX_REGIONS: usize = if cfg!(kani) { 1 } else { 64 };
pub const MAX_REGION_MAPPINGS: usize = if cfg!(kani) { 2 } else { 4 };
pub struct MemoryRegion { /// Where a region's frames come from.
id: RegionId, #[derive(Debug, Clone, Copy, PartialEq, Eq)]
owner: ProcessId, pub enum RegionBacking {
base: PhysicalAddress, /// Frames from the user pool, freed with the region.
size: PageCount, Pool,
generation: Generation, /// Device MMIO; never freed.
Device,
} }
impl MemoryRegion { #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RegionMapping {
pub process: ProcessId,
pub va: VirtualAddress,
pub perms: PagePerms,
/// The capability the mapping was made through. Deleting that
/// capability removes the mapping; `None` for kernel-made mappings.
pub via: Option<CapRef>,
}
/// Physically contiguous memory that may be mapped into several address
/// spaces. `owner` is charged for pool-backed frames.
pub struct Region {
owner: ProcessId,
backing: RegionBacking,
base: PhysicalAddress,
pages: PageCount,
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
}
impl Region {
pub fn new( pub fn new(
id: RegionId,
owner: ProcessId, owner: ProcessId,
backing: RegionBacking,
base: PhysicalAddress, base: PhysicalAddress,
size: PageCount, pages: PageCount,
generation: Generation,
) -> Self { ) -> Self {
Self { Self {
id,
owner, owner,
backing,
base, base,
size, pages,
generation, mappings: [None; MAX_REGION_MAPPINGS],
} }
} }
pub fn id(&self) -> RegionId {
self.id
}
pub fn owner(&self) -> ProcessId { pub fn owner(&self) -> ProcessId {
self.owner self.owner
} }
pub fn backing(&self) -> RegionBacking {
self.backing
}
pub fn base(&self) -> PhysicalAddress { pub fn base(&self) -> PhysicalAddress {
self.base self.base
} }
pub fn size(&self) -> PageCount { pub fn pages(&self) -> PageCount {
self.size self.pages
} }
pub fn generation(&self) -> &Generation { pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
&self.generation self.mappings.iter().flatten()
} }
pub fn revoke(&mut self) { /// Records that `mapping.process` maps this region. Each process maps a
self.generation = self.generation.next(); /// region at most once.
pub fn add_mapping(&mut self, mapping: RegionMapping) -> bool {
if self.mappings().any(|m| m.process == mapping.process) {
return false;
}
match self.mappings.iter_mut().find(|m| m.is_none()) {
Some(slot) => {
*slot = Some(mapping);
true
}
None => false,
}
}
pub fn remove_mapping(&mut self, process: ProcessId) -> Option<RegionMapping> {
self.mappings
.iter_mut()
.find(|m| m.is_some_and(|m| m.process == process))?
.take()
}
/// Whether `va` falls inside this region's mapping in `process`.
pub fn covers(&self, process: ProcessId, va: VirtualAddress) -> bool {
let len = self.pages.value() as usize * frame::PAGE_SIZE;
self.mappings()
.any(|m| m.process == process && va.raw() >= m.va.raw() && va.raw() - m.va.raw() < len)
} }
} }
pub struct RegionTable { pub struct RegionTable {
regions: [Option<MemoryRegion>; MAX_REGIONS], regions: [Option<Region>; MAX_REGIONS],
generations: SlotGenerations<MAX_REGIONS>,
} }
impl RegionTable { impl RegionTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
regions: [const { None }; MAX_REGIONS], regions: [const { None }; MAX_REGIONS],
generations: SlotGenerations::new(),
} }
} }
pub fn insert(&mut self, region: MemoryRegion) -> Option<RegionId> { /// Generation capabilities to a live region must carry.
let idx = self.regions.iter().position(|s| s.is_none())?; pub fn generation(&self, id: RegionId) -> Option<Generation> {
let id = RegionId::from_raw(idx as u16); self.get(id)?;
self.regions[idx] = Some(region); self.generations.current(id.raw() as usize)
Some(id)
} }
pub fn get(&self, id: RegionId) -> Option<&MemoryRegion> { fn free_slot(&self) -> Option<usize> {
self.regions.get(id.raw() as usize).and_then(|s| s.as_ref())
}
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut MemoryRegion> {
self.regions.get_mut(id.raw() as usize).and_then(|s| s.as_mut())
}
pub fn remove(&mut self, id: RegionId) -> Option<MemoryRegion> {
self.regions.get_mut(id.raw() as usize).and_then(|s| s.take())
}
pub fn find_by_base(
&self,
owner: ProcessId,
base: PhysicalAddress,
) -> Option<(RegionId, &MemoryRegion)> {
for (i, slot) in self.regions.iter().enumerate() {
if let Some(region) = slot {
if region.owner() == owner && region.base() == base {
return Some((RegionId::from_raw(i as u16), region));
}
}
}
None
}
pub fn regions_for_process(&self, process: ProcessId) -> impl Iterator<Item = &MemoryRegion> {
self.regions self.regions
.iter() .iter()
.filter_map(|s| s.as_ref()) .enumerate()
.filter(move |r| r.owner() == process) .position(|(i, r)| r.is_none() && self.generations.is_usable(i))
}
/// The id the next `insert` will return.
pub fn next_free_id(&self) -> Option<RegionId> {
let idx = self.free_slot()?;
Some(RegionId::from_raw(idx as u16))
}
pub fn insert(&mut self, region: Region) -> Option<RegionId> {
let idx = self.free_slot()?;
*self.regions.get_mut(idx)? = Some(region);
Some(RegionId::from_raw(idx as u16))
}
pub fn get(&self, id: RegionId) -> Option<&Region> {
self.regions.get(id.raw() as usize)?.as_ref()
}
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut Region> {
self.regions.get_mut(id.raw() as usize)?.as_mut()
}
/// Removes the region; capabilities to it become invalid even if the
/// slot is later reused.
pub fn remove(&mut self, id: RegionId) -> Option<Region> {
let region = self.regions.get_mut(id.raw() as usize)?.take()?;
self.generations.advance(id.raw() as usize);
Some(region)
}
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {
self.regions.iter().enumerate().filter_map(move |(i, r)| {
r.as_ref()
.filter(|r| r.owner == owner)
.map(|_| RegionId::from_raw(i as u16))
})
}
pub fn iter(&self) -> impl Iterator<Item = (RegionId, &Region)> {
self.regions
.iter()
.enumerate()
.filter_map(|(i, r)| r.as_ref().map(|r| (RegionId::from_raw(i as u16), r)))
}
/// Every region, by id, so callers can drop a process's mappings.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut Region)> {
self.regions
.iter_mut()
.enumerate()
.filter_map(|(i, r)| r.as_mut().map(|r| (RegionId::from_raw(i as u16), r)))
}
/// The region whose mapping in `process` covers `va`.
pub fn covering(&self, process: ProcessId, va: VirtualAddress) -> Option<RegionId> {
self.regions.iter().enumerate().find_map(|(i, r)| {
r.as_ref()
.filter(|r| r.covers(process, va))
.map(|_| RegionId::from_raw(i as u16))
})
}
}
impl Default for RegionTable {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -1,68 +0,0 @@
use types::address::PhysicalAddress;
const MAX_PAGES: usize = 16384;
pub struct PagePool {
pages: [bool; MAX_PAGES],
base: PhysicalAddress,
total: usize,
free_count: usize,
}
impl PagePool {
pub fn new(base: PhysicalAddress, total_pages: usize) -> Self {
let mut pages = [false; MAX_PAGES];
for i in 0..total_pages {
pages[i] = true;
}
Self {
pages,
base,
total: total_pages,
free_count: total_pages,
}
}
pub fn allocate(&mut self, count: usize) -> Option<PhysicalAddress> {
let start = self.find_contiguous(count)?;
for i in start..start + count {
self.pages[i] = false;
}
self.free_count -= count;
Some(self.base.offset(start))
}
pub fn free(&mut self, addr: PhysicalAddress, count: usize) -> bool {
if addr.raw() < self.base.raw() {
return false;
}
let start = (addr.raw() - self.base.raw()) / 4096;
if start + count > self.total {
return false;
}
for i in start..start + count {
self.pages[i] = true;
}
self.free_count += count;
true
}
pub fn free_count(&self) -> usize {
self.free_count
}
fn find_contiguous(&self, count: usize) -> Option<usize> {
let mut run = 0;
for i in 0..self.total {
if self.pages[i] {
run += 1;
if run == count {
return Some(i + 1 - count);
}
} else {
run = 0;
}
}
None
}
}

View File

@@ -1,77 +0,0 @@
use types::address::{PhysicalAddress, VirtualAddress};
use types::id::ProcessId;
use crate::error::KernelError;
const USER_HEAP_START: usize = 0x40000;
const PAGE_SIZE: usize = 4096;
const MAX_ALLOCATIONS: usize = 16;
pub struct ProcessMemory {
process: ProcessId,
page_count: usize,
limit: usize,
next_heap_va: usize,
allocations: [(PhysicalAddress, usize); MAX_ALLOCATIONS],
alloc_count: usize,
}
impl ProcessMemory {
pub const DEFAULT_PAGE_LIMIT: usize = 1024;
pub fn new(process: ProcessId, limit: usize) -> Self {
Self {
process,
page_count: 0,
limit,
next_heap_va: USER_HEAP_START,
allocations: [(PhysicalAddress::from_raw(0), 0); MAX_ALLOCATIONS],
alloc_count: 0,
}
}
pub fn alloc_user_va(&mut self, pages: usize) -> VirtualAddress {
let va = self.next_heap_va;
self.next_heap_va += pages * PAGE_SIZE;
VirtualAddress::from_raw(va)
}
pub fn record_allocation(&mut self, pa: PhysicalAddress, count: usize) {
if self.alloc_count < MAX_ALLOCATIONS {
self.allocations[self.alloc_count] = (pa, count);
self.alloc_count += 1;
}
}
pub fn allocations(&self) -> &[(PhysicalAddress, usize)] {
&self.allocations[..self.alloc_count]
}
pub fn process(&self) -> ProcessId {
self.process
}
pub fn page_count(&self) -> usize {
self.page_count
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn grant(&mut self, count: usize) -> Result<(), KernelError> {
if self.page_count + count > self.limit {
return Err(KernelError::OutOfMemory);
}
self.page_count += count;
Ok(())
}
pub fn release(&mut self, count: usize) -> Result<(), KernelError> {
if count > self.page_count {
return Err(KernelError::OutOfMemory);
}
self.page_count -= count;
Ok(())
}
}

View File

@@ -0,0 +1,57 @@
/// Per-process page budget. Every frame charged to a process (user pages
/// and its page tables) counts against it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quota {
used: usize,
limit: usize,
}
impl Quota {
pub const DEFAULT_LIMIT: usize = 1024;
pub const fn new(limit: usize) -> Self {
Self { used: 0, limit }
}
pub fn used(&self) -> usize {
self.used
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn charge(&mut self, pages: usize) -> bool {
match self.used.checked_add(pages) {
Some(total) if total <= self.limit => {
self.used = total;
true
}
_ => false,
}
}
pub fn uncharge(&mut self, pages: usize) -> bool {
match self.used.checked_sub(pages) {
Some(rest) => {
self.used = rest;
true
}
None => false,
}
}
pub fn set_limit(&mut self, limit: usize) -> bool {
if limit < self.used {
return false;
}
self.limit = limit;
true
}
}
impl Default for Quota {
fn default() -> Self {
Self::new(Self::DEFAULT_LIMIT)
}
}

View File

@@ -0,0 +1,37 @@
use super::quota::Quota;
#[test]
fn charge_within_limit_succeeds() {
let mut q = Quota::new(4);
assert!(q.charge(3));
assert_eq!(q.used(), 3);
assert!(q.charge(1));
assert_eq!(q.used(), 4);
}
#[test]
fn charge_past_limit_fails_and_changes_nothing() {
let mut q = Quota::new(4);
q.charge(3);
assert!(!q.charge(2));
assert_eq!(q.used(), 3);
}
#[test]
fn uncharge_cannot_underflow() {
let mut q = Quota::new(4);
q.charge(1);
assert!(!q.uncharge(2));
assert_eq!(q.used(), 1);
assert!(q.uncharge(1));
assert_eq!(q.used(), 0);
}
#[test]
fn limit_can_be_raised_but_not_below_usage() {
let mut q = Quota::new(4);
q.charge(3);
assert!(!q.set_limit(2));
assert!(q.set_limit(10));
assert!(q.charge(7));
}

View File

@@ -1,207 +1,118 @@
use super::page_pool::PagePool;
use super::process_memory::ProcessMemory;
use super::*; use super::*;
use types::address::PhysicalAddress; use types::address::{PhysicalAddress, VirtualAddress};
use types::bounded::PageCount; use types::bounded::PageCount;
use types::generation::Generation;
use types::id::{ProcessId, RegionId}; use types::id::{ProcessId, RegionId};
use types::mapping::PagePerms;
fn base() -> PhysicalAddress { fn pid(n: u16) -> ProcessId {
PhysicalAddress::from_raw(0x8000_0000) ProcessId::from_raw(n)
} }
fn process(id: u16) -> ProcessId { fn va(addr: usize) -> VirtualAddress {
ProcessId::from_raw(id) VirtualAddress::from_raw(addr)
} }
fn pages(n: u16) -> PageCount { fn pool_region(owner: u16, pages: u16) -> Region {
PageCount::new(n).unwrap() Region::new(
pid(owner),
RegionBacking::Pool,
PhysicalAddress::from_raw(0x8400_0000),
PageCount::new(pages).unwrap(),
)
} }
// --- PagePool tests --- fn mapping(process: u16, addr: usize) -> RegionMapping {
RegionMapping {
#[test] process: pid(process),
fn new_pool_has_all_pages_free() { va: va(addr),
let pool = PagePool::new(base(), 100); perms: PagePerms::ReadWrite,
assert_eq!(pool.free_count(), 100); via: None,
}
} }
#[test] #[test]
fn allocate_reduces_free_count() { fn new_region_has_no_mappings() {
let mut pool = PagePool::new(base(), 100); let r = pool_region(0, 4);
pool.allocate(10).unwrap(); assert_eq!(r.mappings().count(), 0);
assert_eq!(pool.free_count(), 90); assert_eq!(r.owner(), pid(0));
assert_eq!(r.backing(), RegionBacking::Pool);
} }
#[test] #[test]
fn allocate_returns_base_aligned_address() { fn a_process_maps_a_region_at_most_once() {
let mut pool = PagePool::new(base(), 100); let mut r = pool_region(0, 4);
let addr = pool.allocate(1).unwrap(); assert!(r.add_mapping(mapping(1, 0x30000)));
assert_eq!(addr, base()); assert!(!r.add_mapping(mapping(1, 0x50000)));
assert!(r.add_mapping(mapping(2, 0x30000)));
assert_eq!(r.mappings().count(), 2);
} }
#[test] #[test]
fn allocate_sequential_returns_contiguous() { fn mapping_slots_are_bounded() {
let mut pool = PagePool::new(base(), 100); let mut r = pool_region(0, 1);
let a = pool.allocate(4).unwrap(); for p in 0..MAX_REGION_MAPPINGS as u16 {
let b = pool.allocate(4).unwrap(); assert!(r.add_mapping(mapping(p, 0x30000)));
assert_eq!(a, base()); }
assert_eq!(b, base().offset(4)); assert!(!r.add_mapping(mapping(99, 0x30000)));
} }
#[test] #[test]
fn allocate_too_many_returns_none() { fn remove_mapping_returns_it() {
let mut pool = PagePool::new(base(), 10); let mut r = pool_region(0, 4);
assert!(pool.allocate(11).is_none()); r.add_mapping(mapping(1, 0x30000));
assert_eq!(r.remove_mapping(pid(1)), Some(mapping(1, 0x30000)));
assert_eq!(r.remove_mapping(pid(1)), None);
} }
#[test] #[test]
fn free_then_reallocate() { fn covers_checks_every_page_of_a_mapping() {
let mut pool = PagePool::new(base(), 10); let mut r = pool_region(0, 4);
let addr = pool.allocate(5).unwrap(); r.add_mapping(mapping(1, 0x30000));
assert_eq!(pool.free_count(), 5); assert!(r.covers(pid(1), va(0x30000)));
assert!(pool.free(addr, 5)); assert!(r.covers(pid(1), va(0x33000)));
assert_eq!(pool.free_count(), 10); assert!(!r.covers(pid(1), va(0x34000)));
let addr2 = pool.allocate(5).unwrap(); assert!(!r.covers(pid(1), va(0x2F000)));
assert_eq!(addr2, base()); assert!(!r.covers(pid(2), va(0x30000)));
} }
#[test] #[test]
fn free_out_of_range_returns_false() { fn region_table_insert_get_remove() {
let mut pool = PagePool::new(base(), 10); let mut t = RegionTable::new();
assert!(!pool.free(PhysicalAddress::from_raw(0), 1)); let id = t.insert(pool_region(0, 2)).unwrap();
assert_eq!(t.get(id).map(|r| r.owner()), Some(pid(0)));
assert!(t.remove(id).is_some());
assert!(t.get(id).is_none());
} }
#[test] #[test]
fn allocate_fragmented_finds_gap() { fn region_table_is_bounded() {
let mut pool = PagePool::new(base(), 10); let mut t = RegionTable::new();
let a = pool.allocate(3).unwrap(); for _ in 0..MAX_REGIONS {
let _b = pool.allocate(3).unwrap(); assert!(t.insert(pool_region(0, 1)).is_some());
let _c = pool.allocate(3).unwrap(); }
pool.free(a, 3); assert!(t.insert(pool_region(0, 1)).is_none());
// now pages 0-2 are free, 3-8 allocated, 9 free
// asking for 3 contiguous should find 0-2
let d = pool.allocate(3).unwrap();
assert_eq!(d, base());
} }
#[test] #[test]
fn allocate_exact_remaining() { fn region_table_finds_regions_owned_by_a_process() {
let mut pool = PagePool::new(base(), 5); let mut t = RegionTable::new();
let addr = pool.allocate(5).unwrap(); let a = t.insert(pool_region(1, 1)).unwrap();
assert_eq!(addr, base()); t.insert(pool_region(2, 1)).unwrap();
assert_eq!(pool.free_count(), 0); let c = t.insert(pool_region(1, 1)).unwrap();
assert!(pool.allocate(1).is_none()); let mut ids = [RegionId::from_raw(u16::MAX); 4];
} let mut n = 0;
for id in t.ids_owned_by(pid(1)) {
// --- ProcessMemory tests --- ids[n] = id;
n += 1;
#[test] }
fn new_process_memory_has_zero_pages() { assert_eq!(&ids[..n], &[a, c]);
let pm = ProcessMemory::new(process(0), 100);
assert_eq!(pm.page_count(), 0);
assert_eq!(pm.limit(), 100);
} }
#[test] #[test]
fn grant_pages_within_limit() { fn region_table_finds_mapping_covering_a_va() {
let mut pm = ProcessMemory::new(process(0), 100); let mut t = RegionTable::new();
assert!(pm.grant(10).is_ok()); let id = t.insert(pool_region(1, 2)).unwrap();
assert_eq!(pm.page_count(), 10); t.get_mut(id).unwrap().add_mapping(mapping(3, 0x30000));
} assert_eq!(t.covering(pid(3), va(0x31000)), Some(id));
assert_eq!(t.covering(pid(3), va(0x32000)), None);
#[test]
fn grant_pages_exceeding_limit_fails() {
let mut pm = ProcessMemory::new(process(0), 10);
assert!(pm.grant(11).is_err());
assert_eq!(pm.page_count(), 0);
}
#[test]
fn release_pages_reduces_count() {
let mut pm = ProcessMemory::new(process(0), 100);
pm.grant(20).unwrap();
assert!(pm.release(5).is_ok());
assert_eq!(pm.page_count(), 15);
}
#[test]
fn release_more_than_owned_fails() {
let mut pm = ProcessMemory::new(process(0), 100);
pm.grant(5).unwrap();
assert!(pm.release(10).is_err());
assert_eq!(pm.page_count(), 5);
}
#[test]
fn grant_up_to_exact_limit() {
let mut pm = ProcessMemory::new(process(0), 10);
pm.grant(10).unwrap();
assert!(pm.grant(1).is_err());
}
// --- MemoryRegion tests ---
#[test]
fn memory_region_construction_and_accessors() {
let region = MemoryRegion::new(
RegionId::from_raw(1),
process(0),
base(),
pages(4),
Generation::initial(),
);
assert_eq!(region.id(), RegionId::from_raw(1));
assert_eq!(region.owner(), process(0));
assert_eq!(region.base(), base());
assert_eq!(region.size(), pages(4));
assert_eq!(region.generation(), &Generation::initial());
}
// --- RegionTable tests ---
#[test]
fn region_table_insert_and_get() {
let mut table = RegionTable::new();
let id = table.insert(MemoryRegion::new(
RegionId::from_raw(0),
process(0),
base(),
pages(4),
Generation::initial(),
));
assert!(id.is_some());
let rid = id.unwrap();
assert!(table.get(rid).is_some());
}
#[test]
fn region_table_remove() {
let mut table = RegionTable::new();
let id = table.insert(MemoryRegion::new(
RegionId::from_raw(0),
process(0),
base(),
pages(4),
Generation::initial(),
)).unwrap();
let removed = table.remove(id);
assert!(removed.is_some());
assert!(table.get(id).is_none());
}
#[test]
fn region_table_regions_for_process() {
let mut table = RegionTable::new();
table.insert(MemoryRegion::new(
RegionId::from_raw(0), process(0), base(), pages(4), Generation::initial(),
));
table.insert(MemoryRegion::new(
RegionId::from_raw(1), process(1), base(), pages(4), Generation::initial(),
));
table.insert(MemoryRegion::new(
RegionId::from_raw(2), process(0), base(), pages(2), Generation::initial(),
));
assert_eq!(table.regions_for_process(process(0)).count(), 2);
assert_eq!(table.regions_for_process(process(1)).count(), 1);
} }

View File

@@ -5,72 +5,38 @@ use types::bounded::CapSlot;
mod tests; mod tests;
pub const MAX_MESSAGE_WORDS: usize = 4; pub const MAX_MESSAGE_WORDS: usize = 4;
pub const MAX_CAP_TRANSFERS: usize = 4;
/// Four words and at most one capability (named by the sender's slot).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Message { pub struct Message {
words: [usize; MAX_MESSAGE_WORDS], words: [usize; MAX_MESSAGE_WORDS],
caps: [Option<CapSlot>; MAX_CAP_TRANSFERS], cap: Option<CapSlot>,
} }
impl Message { impl Message {
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self::from_words([0; MAX_MESSAGE_WORDS])
words: [0; MAX_MESSAGE_WORDS],
caps: [None; MAX_CAP_TRANSFERS],
}
} }
pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self { pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self {
Self { Self { words, cap: None }
words, }
caps: [None; MAX_CAP_TRANSFERS],
} pub fn with_cap(mut self, slot: CapSlot) -> Self {
self.cap = Some(slot);
self
} }
pub fn words(&self) -> &[usize; MAX_MESSAGE_WORDS] { pub fn words(&self) -> &[usize; MAX_MESSAGE_WORDS] {
&self.words &self.words
} }
/// Word `index`, or 0 past the end.
pub fn word(&self, index: usize) -> usize { pub fn word(&self, index: usize) -> usize {
self.words[index] self.words.get(index).copied().unwrap_or(0)
} }
pub fn caps(&self) -> &[Option<CapSlot>; MAX_CAP_TRANSFERS] { pub fn cap(&self) -> Option<CapSlot> {
&self.caps self.cap
}
pub fn cap(&self, index: usize) -> Option<CapSlot> {
self.caps[index]
}
pub fn builder() -> MessageBuilder {
MessageBuilder {
message: Message::empty(),
}
}
}
pub struct MessageBuilder {
message: Message,
}
impl MessageBuilder {
pub fn word(mut self, index: usize, value: usize) -> Self {
self.message.words[index] = value;
self
}
pub fn words(mut self, words: [usize; MAX_MESSAGE_WORDS]) -> Self {
self.message.words = words;
self
}
pub fn cap(mut self, index: usize, slot: CapSlot) -> Self {
self.message.caps[index] = Some(slot);
self
}
pub fn build(self) -> Message {
self.message
} }
} }

View File

@@ -1,86 +1,23 @@
use super::*; use super::*;
use types::bounded::CapSlot; use types::bounded::CapSlot;
fn slot(n: u8) -> CapSlot {
CapSlot::new(n).unwrap()
}
// --- Construction ---
#[test] #[test]
fn empty_message_has_zeroed_words_and_no_caps() { fn empty_message_has_zeroed_words_and_no_cap() {
let msg = Message::empty(); let msg = Message::empty();
assert_eq!(msg.words(), &[0; MAX_MESSAGE_WORDS]); assert_eq!(msg.words(), &[0; MAX_MESSAGE_WORDS]);
for i in 0..MAX_CAP_TRANSFERS { assert!(msg.cap().is_none());
assert!(msg.cap(i).is_none());
}
} }
#[test] #[test]
fn from_words_sets_words_and_no_caps() { fn from_words_keeps_every_word() {
let msg = Message::from_words([10, 20, 30, 40]); let msg = Message::from_words([10, 20, 30, 40]);
assert_eq!(msg.word(0), 10); assert_eq!(msg.words(), &[10, 20, 30, 40]);
assert_eq!(msg.word(1), 20); assert!(msg.cap().is_none());
assert_eq!(msg.word(2), 30);
assert_eq!(msg.word(3), 40);
assert!(msg.cap(0).is_none());
}
// --- Accessors ---
#[test]
fn words_returns_full_array() {
let msg = Message::from_words([1, 2, 3, 4]);
assert_eq!(msg.words(), &[1, 2, 3, 4]);
} }
#[test] #[test]
fn caps_returns_full_array() { fn with_cap_attaches_one_capability() {
let msg = Message::empty(); let msg = Message::from_words([1, 2, 3, 4]).with_cap(CapSlot::new(5).unwrap());
assert_eq!(msg.caps(), &[None; MAX_CAP_TRANSFERS]); assert_eq!(msg.cap(), CapSlot::new(5));
} assert_eq!(msg.word(3), 4);
// --- Builder ---
#[test]
fn builder_sets_individual_words() {
let msg = Message::builder()
.word(0, 42)
.word(2, 99)
.build();
assert_eq!(msg.word(0), 42);
assert_eq!(msg.word(1), 0);
assert_eq!(msg.word(2), 99);
assert_eq!(msg.word(3), 0);
}
#[test]
fn builder_sets_caps() {
let msg = Message::builder()
.cap(0, slot(5))
.cap(1, slot(10))
.build();
assert_eq!(msg.cap(0), Some(slot(5)));
assert_eq!(msg.cap(1), Some(slot(10)));
assert!(msg.cap(2).is_none());
}
#[test]
fn builder_sets_words_and_caps_together() {
let msg = Message::builder()
.word(0, 100)
.word(1, 200)
.cap(0, slot(3))
.build();
assert_eq!(msg.word(0), 100);
assert_eq!(msg.word(1), 200);
assert_eq!(msg.cap(0), Some(slot(3)));
}
#[test]
fn builder_with_all_words() {
let msg = Message::builder()
.words([11, 22, 33, 44])
.build();
assert_eq!(msg.words(), &[11, 22, 33, 44]);
} }

View File

@@ -0,0 +1,192 @@
//! Asynchronous signals: a word of bits, a FIFO of waiters and at most one
//! bound thread. See docs/ipc.md.
use types::{
badge::Badge,
generation::Generation,
id::{NotificationId, ProcessId, ThreadId},
};
use crate::{endpoint::wait_queue::WaitQueue, generations::SlotGenerations};
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(kani)]
#[path = "proofs.rs"]
mod proofs;
pub const MAX_NOTIFICATIONS: usize = if cfg!(kani) { 1 } else { 128 };
/// What the kernel must do after a signal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalEffect {
/// Wake this blocked `Wait`er with these bits.
WakeWaiter(ThreadId, usize),
/// Wake the bound thread out of its `Receive` with these bits.
WakeBound(ThreadId, usize),
/// Nobody to wake; the bits were stored.
Stored,
}
pub struct Notification {
id: NotificationId,
owner: ProcessId,
bits: usize,
waiters: WaitQueue,
bound: Option<ThreadId>,
}
impl Notification {
pub fn new(id: NotificationId, owner: ProcessId) -> Self {
Self {
id,
owner,
bits: 0,
waiters: WaitQueue::new(),
bound: None,
}
}
pub fn id(&self) -> NotificationId {
self.id
}
pub fn owner(&self) -> ProcessId {
self.owner
}
pub fn bound(&self) -> Option<ThreadId> {
self.bound
}
/// Delivers a signal. `bound_is_receiving` says whether the bound
/// thread is currently blocked in `Receive` and can be woken.
pub fn signal(&mut self, badge: Badge, bound_is_receiving: bool) -> SignalEffect {
let bits = if badge.is_none() { 1 } else { badge.raw() };
if let Some(waiter) = self.waiters.pop() {
return SignalEffect::WakeWaiter(waiter, bits);
}
if let (Some(bound), true) = (self.bound, bound_is_receiving) {
return SignalEffect::WakeBound(bound, bits);
}
self.bits |= bits;
SignalEffect::Stored
}
/// Returns and clears the pending bits.
pub fn take_bits(&mut self) -> usize {
core::mem::take(&mut self.bits)
}
pub fn has_bits(&self) -> bool {
self.bits != 0
}
pub fn waiters(&self) -> impl Iterator<Item = ThreadId> + '_ {
self.waiters.iter()
}
pub fn is_waiter(&self, thread: ThreadId) -> bool {
self.waiters.contains(thread)
}
pub fn add_waiter(&mut self, thread: ThreadId) -> bool {
self.waiters.push(thread).is_some()
}
pub fn bind(&mut self, thread: ThreadId) -> bool {
if self.bound.is_some() {
return false;
}
self.bound = Some(thread);
true
}
pub fn unbind(&mut self, thread: ThreadId) {
if self.bound == Some(thread) {
self.bound = None;
}
}
/// Forgets `thread` as waiter and as bound thread.
pub fn remove_thread(&mut self, thread: ThreadId) {
self.waiters.remove_thread(thread);
self.unbind(thread);
}
/// Drains every waiter, e.g. when the notification is destroyed.
pub fn pop_waiter(&mut self) -> Option<ThreadId> {
self.waiters.pop()
}
}
pub struct NotificationTable {
slots: [Option<Notification>; MAX_NOTIFICATIONS],
generations: SlotGenerations<MAX_NOTIFICATIONS>,
}
impl NotificationTable {
pub fn new() -> Self {
Self {
slots: [const { None }; MAX_NOTIFICATIONS],
generations: SlotGenerations::new(),
}
}
pub fn create(&mut self, owner: ProcessId) -> Option<NotificationId> {
let generations = &self.generations;
let (idx, slot) = self
.slots
.iter_mut()
.enumerate()
.find(|(i, s)| s.is_none() && generations.is_usable(*i))?;
let id = NotificationId::from_raw(idx as u16);
*slot = Some(Notification::new(id, owner));
Some(id)
}
pub fn get(&self, id: NotificationId) -> Option<&Notification> {
self.slots.get(id.raw() as usize)?.as_ref()
}
pub fn get_mut(&mut self, id: NotificationId) -> Option<&mut Notification> {
self.slots.get_mut(id.raw() as usize)?.as_mut()
}
pub fn generation(&self, id: NotificationId) -> Option<Generation> {
self.get(id)?;
self.generations.current(id.raw() as usize)
}
pub fn destroy(&mut self, id: NotificationId) -> Option<Notification> {
let n = self.slots.get_mut(id.raw() as usize)?.take()?;
self.generations.advance(id.raw() as usize);
Some(n)
}
pub fn iter(&self) -> impl Iterator<Item = &Notification> {
self.slots.iter().flatten()
}
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = NotificationId> + '_ {
self.slots
.iter()
.flatten()
.filter(move |n| n.owner == owner)
.map(|n| n.id)
}
pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
for n in self.slots.iter_mut().flatten() {
n.remove_thread(thread);
}
}
}
impl Default for NotificationTable {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,43 @@
//! Signals are never lost: every signal either wakes exactly one thread
//! with its bits, or leaves those bits set for the next `Wait`.
use super::*;
use types::badge::Badge;
use types::id::{NotificationId, ProcessId, ThreadId};
#[kani::proof]
#[kani::unwind(5)]
fn a_signal_is_delivered_or_stored() {
let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0));
n.bits = kani::any();
if kani::any() {
n.add_waiter(ThreadId::from_raw(kani::any::<u16>() % 4));
}
if kani::any() {
n.bind(ThreadId::from_raw(kani::any::<u16>() % 4));
}
let before = n.bits;
let badge = Badge::new(kani::any());
let expected = if badge.is_none() { 1 } else { badge.raw() };
match n.signal(badge, kani::any()) {
SignalEffect::WakeWaiter(_, bits) | SignalEffect::WakeBound(_, bits) => {
assert!(bits == expected);
assert!(n.bits == before, "woken signals are not also stored");
}
SignalEffect::Stored => {
assert!(n.bits == before | expected);
assert!(n.bits & expected == expected);
}
}
}
#[kani::proof]
#[kani::unwind(5)]
fn take_bits_returns_everything_and_clears() {
let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0));
let bits: usize = kani::any();
n.bits = bits;
assert!(n.take_bits() == bits);
assert!(n.bits == 0);
}

View File

@@ -0,0 +1,116 @@
use super::*;
use types::badge::Badge;
use types::id::{NotificationId, ProcessId, ThreadId};
fn ntfn() -> Notification {
Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0))
}
fn t(n: u16) -> ThreadId {
ThreadId::from_raw(n)
}
#[test]
fn signal_with_nobody_waiting_stores_the_badge() {
let mut n = ntfn();
assert_eq!(n.signal(Badge::new(0b100), false), SignalEffect::Stored);
assert_eq!(n.signal(Badge::new(0b001), false), SignalEffect::Stored);
assert_eq!(n.take_bits(), 0b101);
assert_eq!(n.take_bits(), 0, "taking clears");
}
#[test]
fn unbadged_signal_sets_bit_zero() {
let mut n = ntfn();
n.signal(Badge::NONE, false);
assert_eq!(n.take_bits(), 1);
}
#[test]
fn signal_wakes_the_first_waiter_with_its_badge() {
let mut n = ntfn();
assert!(n.add_waiter(t(1)));
assert!(n.add_waiter(t(2)));
assert_eq!(
n.signal(Badge::new(8), false),
SignalEffect::WakeWaiter(t(1), 8)
);
assert_eq!(n.take_bits(), 0, "delivered bits are not also stored");
assert_eq!(
n.signal(Badge::new(4), false),
SignalEffect::WakeWaiter(t(2), 4)
);
}
#[test]
fn signal_wakes_a_receiving_bound_thread_when_no_waiter() {
let mut n = ntfn();
assert!(n.bind(t(5)));
assert_eq!(
n.signal(Badge::new(2), true),
SignalEffect::WakeBound(t(5), 2)
);
assert_eq!(n.signal(Badge::new(2), false), SignalEffect::Stored);
}
#[test]
fn waiters_take_priority_over_the_bound_thread() {
let mut n = ntfn();
n.bind(t(5));
n.add_waiter(t(1));
assert_eq!(
n.signal(Badge::new(2), true),
SignalEffect::WakeWaiter(t(1), 2)
);
}
#[test]
fn only_one_thread_can_be_bound() {
let mut n = ntfn();
assert!(n.bind(t(1)));
assert!(!n.bind(t(2)));
n.unbind(t(1));
assert!(n.bind(t(2)));
}
#[test]
fn removed_waiter_is_not_woken() {
let mut n = ntfn();
n.add_waiter(t(1));
n.remove_thread(t(1));
assert_eq!(n.signal(Badge::new(1), false), SignalEffect::Stored);
}
#[test]
fn remove_thread_also_unbinds() {
let mut n = ntfn();
n.bind(t(1));
n.remove_thread(t(1));
assert_eq!(n.bound(), None);
}
#[test]
fn table_generations_follow_slot_reuse() {
let mut table = NotificationTable::new();
let owner = ProcessId::from_raw(3);
let id = table.create(owner).unwrap();
let g = table.generation(id).unwrap();
assert!(table.destroy(id).is_some());
assert_eq!(table.generation(id), None);
let again = table.create(owner).unwrap();
assert_eq!(again, id);
assert!(!g.is_valid_against(&table.generation(again).unwrap()));
}
#[test]
fn table_lists_notifications_by_owner() {
let mut table = NotificationTable::new();
let a = table.create(ProcessId::from_raw(1)).unwrap();
table.create(ProcessId::from_raw(2)).unwrap();
let mut n = 0;
for id in table.ids_owned_by(ProcessId::from_raw(1)) {
assert_eq!(id, a);
n += 1;
}
assert_eq!(n, 1);
}

View File

@@ -1,16 +1,17 @@
use types::{ use types::{
bounded::CapSlot, bounded::CapSlot,
generation::Generation,
id::{ProcessId, ThreadId}, id::{ProcessId, ThreadId},
}; };
use crate::{capability::CapabilityTable, error::KernelError}; use crate::{capability::CapabilityTable, error::KernelError, generations::SlotGenerations};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
mod tests; mod tests;
pub const MAX_THREADS_PER_PROCESS: usize = 4; pub const MAX_THREADS_PER_PROCESS: usize = if cfg!(kani) { 2 } else { 4 };
pub const MAX_PROCESSES: usize = 64; pub const MAX_PROCESSES: usize = if cfg!(kani) { 3 } else { 64 };
pub struct Process { pub struct Process {
id: ProcessId, id: ProcessId,
@@ -75,28 +76,38 @@ impl Process {
pub struct ProcessTable { pub struct ProcessTable {
processes: [Option<Process>; MAX_PROCESSES], processes: [Option<Process>; MAX_PROCESSES],
generations: SlotGenerations<MAX_PROCESSES>,
} }
impl ProcessTable { impl ProcessTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
processes: [const { None }; MAX_PROCESSES], processes: [const { None }; MAX_PROCESSES],
generations: SlotGenerations::new(),
} }
} }
/// Generation capabilities to a live process must carry.
pub fn generation(&self, id: ProcessId) -> Option<Generation> {
self.get(id).ok()?;
self.generations.current(id.raw() as usize)
}
pub fn create( pub fn create(
&mut self, &mut self,
capabilities: CapabilityTable, capabilities: CapabilityTable,
fault_handler: Option<CapSlot>, fault_handler: Option<CapSlot>,
) -> Result<ProcessId, KernelError> { ) -> Result<ProcessId, KernelError> {
let idx = self let generations = &self.generations;
let (idx, slot) = self
.processes .processes
.iter() .iter_mut()
.position(|s| s.is_none()) .enumerate()
.find(|(i, s)| s.is_none() && generations.is_usable(*i))
.ok_or(KernelError::TooManyProcesses)?; .ok_or(KernelError::TooManyProcesses)?;
let id = ProcessId::from_raw(idx as u16); let id = ProcessId::from_raw(idx as u16);
self.processes[idx] = Some(Process::new(id, capabilities, fault_handler)); *slot = Some(Process::new(id, capabilities, fault_handler));
Ok(id) Ok(id)
} }
@@ -116,11 +127,22 @@ impl ProcessTable {
.ok_or(KernelError::InvalidProcess(id)) .ok_or(KernelError::InvalidProcess(id))
} }
/// Removes the process; capabilities to it become invalid even if the
/// pid is later reused.
pub fn destroy(&mut self, id: ProcessId) -> Result<Process, KernelError> { pub fn destroy(&mut self, id: ProcessId) -> Result<Process, KernelError> {
let idx = id.raw() as usize; let idx = id.raw() as usize;
self.processes let process = self
.processes
.get_mut(idx) .get_mut(idx)
.and_then(|s| s.take()) .and_then(|s| s.take())
.ok_or(KernelError::InvalidProcess(id)) .ok_or(KernelError::InvalidProcess(id))?;
self.generations.advance(idx);
Ok(process)
}
}
impl Default for ProcessTable {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -1,6 +1,6 @@
use super::{arbitrary_endpoint_cap, insert_cap};
use crate::capability::CapabilityTable; use crate::capability::CapabilityTable;
use crate::resource::Resource; use crate::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::permission::PermissionSet; use types::permission::PermissionSet;
#[kani::proof] #[kani::proof]

View File

@@ -1,6 +1,6 @@
use super::{arbitrary_endpoint_cap, insert_cap};
use crate::capability::{Capability, CapabilityTable}; use crate::capability::{Capability, CapabilityTable};
use crate::resource::Resource; use crate::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::generation::Generation; use types::generation::Generation;
use types::id::EndpointId; use types::id::EndpointId;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
@@ -16,7 +16,10 @@ fn delegation_via_cap_table_never_escalates() {
kani::assume(source_perms.allows(Permission::Delegate)); kani::assume(source_perms.allows(Permission::Delegate));
let mut source_table = CapabilityTable::new(); let mut source_table = CapabilityTable::new();
let slot = insert_cap(&mut source_table, arbitrary_endpoint_cap(source_bits, ep_id)); let slot = insert_cap(
&mut source_table,
arbitrary_endpoint_cap(source_bits, ep_id),
);
let cap = source_table.get(slot).unwrap(); let cap = source_table.get(slot).unwrap();
let original_perms = *cap.permissions(); let original_perms = *cap.permissions();
@@ -43,11 +46,18 @@ fn delegation_cannot_add_permissions_not_in_source() {
let source = PermissionSet::from_bits(source_bits); let source = PermissionSet::from_bits(source_bits);
let result = source.restrict_to(&PermissionSet::from_bits(restrict_bits)); let result = source.restrict_to(&PermissionSet::from_bits(restrict_bits));
if !source.allows(Permission::Read) { assert!(!result.allows(Permission::Read)); } if !source.allows(Permission::Read) {
if !source.allows(Permission::Write) { assert!(!result.allows(Permission::Write)); } assert!(!result.allows(Permission::Read));
if !source.allows(Permission::Execute) { assert!(!result.allows(Permission::Execute)); } }
if !source.allows(Permission::Delegate) { assert!(!result.allows(Permission::Delegate)); } if !source.allows(Permission::Write) {
if !source.allows(Permission::Revoke) { assert!(!result.allows(Permission::Revoke)); } assert!(!result.allows(Permission::Write));
}
if !source.allows(Permission::Execute) {
assert!(!result.allows(Permission::Execute));
}
if !source.allows(Permission::Delegate) {
assert!(!result.allows(Permission::Delegate));
}
} }
#[kani::proof] #[kani::proof]

View File

@@ -22,7 +22,7 @@ fn dead_thread_rejects_all_transitions() {
assert!(matches!(thread.state(), ThreadState::Dead)); assert!(matches!(thread.state(), ThreadState::Dead));
assert!(thread.set_running(CoreId::from_raw(0)).is_err()); assert!(thread.set_running(CoreId::from_raw(0)).is_err());
assert!(thread.block(BlockReason::WaitingAny).is_err()); assert!(thread.block(BlockReason::AwaitingReply).is_err());
assert!(thread.unblock().is_err()); assert!(thread.unblock().is_err());
assert!(thread.preempt().is_err()); assert!(thread.preempt().is_err());
assert!(thread.exit().is_err()); assert!(thread.exit().is_err());
@@ -36,7 +36,7 @@ fn only_running_thread_can_block() {
let mut thread = test_thread(Priority::new(prio_val).unwrap()); let mut thread = test_thread(Priority::new(prio_val).unwrap());
assert!(matches!(thread.state(), ThreadState::Ready)); assert!(matches!(thread.state(), ThreadState::Ready));
assert!(thread.block(BlockReason::WaitingAny).is_err()); assert!(thread.block(BlockReason::AwaitingReply).is_err());
} }
#[kani::proof] #[kani::proof]
@@ -44,7 +44,7 @@ fn blocked_thread_cannot_run_directly() {
let mut thread = test_thread(Priority::new(16).unwrap()); let mut thread = test_thread(Priority::new(16).unwrap());
thread.set_running(CoreId::from_raw(0)).unwrap(); thread.set_running(CoreId::from_raw(0)).unwrap();
thread.block(BlockReason::WaitingAny).unwrap(); thread.block(BlockReason::AwaitingReply).unwrap();
assert!(thread.set_running(CoreId::from_raw(0)).is_err()); assert!(thread.set_running(CoreId::from_raw(0)).is_err());
} }

View File

@@ -1,10 +1,13 @@
use types::id::{EndpointId, IrqId, ProcessId, RegionId}; use types::id::{EndpointId, IrqId, NotificationId, ProcessId, RegionId};
#[derive(Clone)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resource { pub enum Resource {
Memory(RegionId), Memory(RegionId),
Endpoint(EndpointId), Endpoint(EndpointId),
Notification(NotificationId),
Process(ProcessId), Process(ProcessId),
Interrupt(IrqId), Interrupt(IrqId),
ProcessCreate, ProcessCreate,
/// Machine-wide controls such as halting; given only to init at boot.
SystemControl,
} }

View File

@@ -3,10 +3,10 @@ use types::{
id::{CoreId, ThreadId}, id::{CoreId, ThreadId},
}; };
use super::{per_core::PerCoreScheduler, Scheduler, SchedulerDecision}; use super::{Scheduler, SchedulerDecision, per_core::PerCoreScheduler};
const MAX_CORES: usize = 8; const MAX_CORES: usize = if cfg!(kani) { 1 } else { 8 };
const MAX_SCHEDULED_THREADS: usize = 256; const MAX_SCHEDULED_THREADS: usize = crate::thread::MAX_THREADS;
const BALANCE_THRESHOLD: usize = 2; const BALANCE_THRESHOLD: usize = 2;
struct ThreadPlacement { struct ThreadPlacement {
@@ -25,17 +25,64 @@ impl MultiCoreScheduler {
let cores = core::array::from_fn(|i| PerCoreScheduler::new(CoreId::from_raw(i as u8))); let cores = core::array::from_fn(|i| PerCoreScheduler::new(CoreId::from_raw(i as u8)));
Self { Self {
cores, cores,
num_cores, num_cores: num_cores.min(MAX_CORES as u8),
placements: [const { None }; MAX_SCHEDULED_THREADS], placements: [const { None }; MAX_SCHEDULED_THREADS],
} }
} }
fn core(&self, core: CoreId) -> Option<&PerCoreScheduler> {
self.active().get(core.raw() as usize)
}
fn core_mut(&mut self, core: CoreId) -> Option<&mut PerCoreScheduler> {
let n = self.num_cores as usize;
self.cores.get_mut(..n)?.get_mut(core.raw() as usize)
}
fn active(&self) -> &[PerCoreScheduler] {
self.cores.get(..self.num_cores as usize).unwrap_or(&[])
}
fn placement(&self, thread: ThreadId) -> Option<&ThreadPlacement> {
self.placements.get(thread.raw() as usize)?.as_ref()
}
pub fn num_cores(&self) -> u8 {
self.num_cores
}
/// The highest priority waiting in `core`'s run queue.
pub fn highest_waiting(&self, core: CoreId) -> Option<Priority> {
self.core(core)?.highest_waiting_priority()
}
/// The core whose run queue holds `thread`, if any.
pub fn queued_on(&self, thread: ThreadId) -> Option<CoreId> {
self.active()
.iter()
.find(|c| c.is_queued(thread))
.map(|c| c.core())
}
/// Whether `thread` waits in some core's run queue.
pub fn is_queued(&self, thread: ThreadId) -> bool {
self.active().iter().any(|c| c.is_queued(thread))
}
pub fn current(&self, core: CoreId) -> Option<ThreadId> { pub fn current(&self, core: CoreId) -> Option<ThreadId> {
self.cores[core.raw() as usize].current() self.core(core)?.current()
} }
pub fn set_current(&mut self, core: CoreId, thread: ThreadId) { pub fn set_current(&mut self, core: CoreId, thread: ThreadId) {
self.cores[core.raw() as usize].set_current(thread); if let Some(c) = self.core_mut(core) {
c.set_current(thread);
}
}
/// Runs `thread` next on `core` regardless of its place in the queue.
/// Fails if it is not queued there.
pub fn run_now(&mut self, core: CoreId, thread: ThreadId) -> bool {
self.core_mut(core).is_some_and(|c| c.run_now(thread))
} }
fn thread_count_on(&self, core: CoreId) -> usize { fn thread_count_on(&self, core: CoreId) -> usize {
@@ -47,52 +94,62 @@ impl MultiCoreScheduler {
fn least_loaded_core(&self) -> Option<CoreId> { fn least_loaded_core(&self) -> Option<CoreId> {
(0..self.num_cores) (0..self.num_cores)
.map(|i| CoreId::from_raw(i)) .map(CoreId::from_raw)
.min_by_key(|&c| self.thread_count_on(c)) .min_by_key(|&c| self.thread_count_on(c))
} }
fn most_loaded_core(&self) -> Option<CoreId> { fn most_loaded_core(&self) -> Option<CoreId> {
(0..self.num_cores) (0..self.num_cores)
.map(|i| CoreId::from_raw(i)) .map(CoreId::from_raw)
.max_by_key(|&c| self.thread_count_on(c)) .max_by_key(|&c| self.thread_count_on(c))
} }
} }
impl Scheduler for MultiCoreScheduler { impl Scheduler for MultiCoreScheduler {
fn add(&mut self, thread: ThreadId, priority: Priority, core: CoreId) { fn add(&mut self, thread: ThreadId, priority: Priority, core: CoreId) {
let idx = thread.raw() as usize; if self.core(core).is_none() {
self.placements[idx] = Some(ThreadPlacement { core, priority }); return;
self.cores[core.raw() as usize].add(thread, priority); }
let Some(placement) = self.placements.get_mut(thread.raw() as usize) else {
return;
};
*placement = Some(ThreadPlacement { core, priority });
if let Some(c) = self.core_mut(core) {
c.add(thread, priority);
}
} }
fn remove(&mut self, thread: ThreadId) { fn remove(&mut self, thread: ThreadId) {
let idx = thread.raw() as usize; let Some(placement) = self
if let Some(placement) = self.placements[idx].take() { .placements
let core_idx = placement.core.raw() as usize; .get_mut(thread.raw() as usize)
self.cores[core_idx].remove(thread, placement.priority); .and_then(Option::take)
if self.cores[core_idx].current() == Some(thread) { else {
self.cores[core_idx].clear_current(); return;
};
if let Some(c) = self.core_mut(placement.core) {
c.remove(thread);
if c.current() == Some(thread) {
c.clear_current();
} }
} }
} }
fn next(&mut self, core: CoreId) -> Option<ThreadId> { fn next(&mut self, core: CoreId) -> Option<ThreadId> {
self.cores[core.raw() as usize].pick_next() self.core_mut(core)?.pick_next()
} }
fn tick(&mut self, core: CoreId, current: ThreadId) -> SchedulerDecision { fn tick(&mut self, core: CoreId, current: ThreadId) -> SchedulerDecision {
let idx = current.raw() as usize; let Some(current_priority) = self.placement(current).map(|p| p.priority) else {
if let Some(placement) = &self.placements[idx] { return SchedulerDecision::Continue;
let current_priority = placement.priority; };
let core_idx = core.raw() as usize; if let Some(c) = self.core_mut(core)
if let Some(highest) = self.cores[core_idx].highest_waiting_priority() { && let Some(highest) = c.highest_waiting_priority()
if highest > current_priority { && highest > current_priority
if let Some(next) = self.cores[core_idx].pick_next() { && let Some(next) = c.pick_next()
self.cores[core_idx].add(current, current_priority); {
return SchedulerDecision::Preempt(next); c.add(current, current_priority);
} return SchedulerDecision::Preempt(next);
}
}
} }
SchedulerDecision::Continue SchedulerDecision::Continue
} }
@@ -103,13 +160,14 @@ impl Scheduler for MultiCoreScheduler {
_ => return, _ => return,
}; };
if self.thread_count_on(busiest) > self.thread_count_on(lightest) + BALANCE_THRESHOLD { if self.thread_count_on(busiest) > self.thread_count_on(lightest) + BALANCE_THRESHOLD
let busiest_idx = busiest.raw() as usize; && let Some((thread, priority)) = self.core_mut(busiest).and_then(|c| c.steal_lowest())
if let Some((thread, priority)) = self.cores[busiest_idx].steal_lowest() { {
let lightest_idx = lightest.raw() as usize; if let Some(c) = self.core_mut(lightest) {
self.cores[lightest_idx].add(thread, priority); c.add(thread, priority);
let tid = thread.raw() as usize; }
self.placements[tid] = Some(ThreadPlacement { if let Some(p) = self.placements.get_mut(thread.raw() as usize) {
*p = Some(ThreadPlacement {
core: lightest, core: lightest,
priority, priority,
}); });

View File

@@ -29,16 +29,29 @@ impl PerCoreScheduler {
self.queue.add(thread, priority); self.queue.add(thread, priority);
} }
pub fn remove(&mut self, thread: ThreadId, priority: Priority) { pub fn is_queued(&self, thread: ThreadId) -> bool {
self.queue.remove(thread, priority); self.queue.contains(thread)
}
pub fn remove(&mut self, thread: ThreadId) {
self.queue.remove(thread);
} }
pub fn pick_next(&mut self) -> Option<ThreadId> { pub fn pick_next(&mut self) -> Option<ThreadId> {
let (thread, _priority) = self.queue.next()?; let (thread, _priority) = self.queue.pop_highest()?;
self.current = Some(thread); self.current = Some(thread);
Some(thread) Some(thread)
} }
/// Takes `thread` out of the run queue and makes it current.
pub fn run_now(&mut self, thread: ThreadId) -> bool {
if !self.queue.remove(thread) {
return false;
}
self.current = Some(thread);
true
}
pub fn clear_current(&mut self) { pub fn clear_current(&mut self) {
self.current = None; self.current = None;
} }
@@ -56,6 +69,10 @@ impl PerCoreScheduler {
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.queue.bitmap().count_ones() as usize self.queue.len()
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
} }
} }

View File

@@ -2,71 +2,42 @@ use types::bounded::Priority;
use types::id::ThreadId; use types::id::ThreadId;
const NUM_PRIORITIES: usize = 32; const NUM_PRIORITIES: usize = 32;
const MAX_THREADS_PER_PRIORITY: usize = 16; const MAX_THREADS: usize = crate::thread::MAX_THREADS;
struct RunList { #[derive(Clone, Copy)]
threads: [Option<ThreadId>; MAX_THREADS_PER_PRIORITY], struct Links {
head: usize, prev: Option<ThreadId>,
tail: usize, next: Option<ThreadId>,
count: u8, queued_at: Option<u8>,
} }
impl RunList { #[derive(Clone, Copy)]
fn new() -> Self { struct ListEnds {
Self { head: Option<ThreadId>,
threads: [None; MAX_THREADS_PER_PRIORITY], tail: Option<ThreadId>,
head: 0,
tail: 0,
count: 0,
}
}
fn push(&mut self, thread: ThreadId) -> bool {
match self.threads.iter_mut().find(|s| s.is_none()) {
Some(slot) => {
*slot = Some(thread);
self.count += 1;
true
}
None => false,
}
}
fn pop(&mut self) -> Option<ThreadId> {
if self.count == 0 {
return None;
}
let slot = self.threads.iter_mut().find(|s| s.is_some())?;
let thread = slot.take();
self.count -= 1;
thread
}
fn remove(&mut self, thread: ThreadId) -> bool {
for slot in &mut self.threads {
if *slot == Some(thread) {
*slot = None;
self.count -= 1;
return true;
}
}
false
}
fn is_empty(&self) -> bool {
self.count == 0
}
} }
/// Ready queue: one FIFO per priority level, threaded through an
/// intrusive doubly linked list indexed by thread id. A thread is in at
/// most one list, so capacity never runs out and every op is O(1).
pub struct PriorityQueue { pub struct PriorityQueue {
queues: [RunList; NUM_PRIORITIES], lists: [ListEnds; NUM_PRIORITIES],
links: [Links; MAX_THREADS],
bitmap: u32, bitmap: u32,
} }
impl PriorityQueue { impl PriorityQueue {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
queues: core::array::from_fn(|_| RunList::new()), lists: [ListEnds {
head: None,
tail: None,
}; NUM_PRIORITIES],
links: [Links {
prev: None,
next: None,
queued_at: None,
}; MAX_THREADS],
bitmap: 0, bitmap: 0,
} }
} }
@@ -75,34 +46,107 @@ impl PriorityQueue {
self.bitmap self.bitmap
} }
pub fn add(&mut self, thread: ThreadId, priority: Priority) { pub fn len(&self) -> usize {
let idx = priority.value() as usize; self.links.iter().filter(|l| l.queued_at.is_some()).count()
let added = self.queues[idx].push(thread);
debug_assert!(added, "scheduler run list full for priority {}", idx);
if added {
self.bitmap |= 1 << idx;
}
} }
pub fn remove(&mut self, thread: ThreadId, priority: Priority) { pub fn contains(&self, thread: ThreadId) -> bool {
self.link(thread).is_some_and(|l| l.queued_at.is_some())
}
fn link(&self, thread: ThreadId) -> Option<&Links> {
self.links.get(thread.raw() as usize)
}
fn links_mut(&mut self, thread: ThreadId) -> Option<&mut Links> {
self.links.get_mut(thread.raw() as usize)
}
/// Appends `thread` at `priority`. A thread that is already queued is
/// left where it is; returns whether it was added.
pub fn add(&mut self, thread: ThreadId, priority: Priority) -> bool {
let idx = priority.value() as usize; let idx = priority.value() as usize;
self.queues[idx].remove(thread); let queued = self.link(thread).is_none_or(|l| l.queued_at.is_some());
if self.queues[idx].is_empty() { let Some(old_tail) = self.lists.get(idx).map(|l| l.tail) else {
return false;
};
if queued {
return false;
}
if let Some(l) = self.links_mut(thread) {
*l = Links {
prev: old_tail,
next: None,
queued_at: Some(idx as u8),
};
}
match old_tail.and_then(|t| self.links_mut(t)) {
Some(tail) => tail.next = Some(thread),
None => {
if let Some(list) = self.lists.get_mut(idx) {
list.head = Some(thread);
}
}
}
if let Some(list) = self.lists.get_mut(idx) {
list.tail = Some(thread);
}
self.bitmap |= 1 << idx;
true
}
/// Unlinks `thread` from whichever level it is queued at.
pub fn remove(&mut self, thread: ThreadId) -> bool {
let Some(Links {
prev,
next,
queued_at: Some(level),
}) = self.link(thread).copied()
else {
return false;
};
let idx = level as usize;
match prev.and_then(|p| self.links_mut(p)) {
Some(p) => p.next = next,
None => {
if let Some(list) = self.lists.get_mut(idx) {
list.head = next;
}
}
}
match next.and_then(|n| self.links_mut(n)) {
Some(n) => n.prev = prev,
None => {
if let Some(list) = self.lists.get_mut(idx) {
list.tail = prev;
}
}
}
if let Some(l) = self.links_mut(thread) {
*l = Links {
prev: None,
next: None,
queued_at: None,
};
}
if self.lists.get(idx).is_none_or(|l| l.head.is_none()) {
self.bitmap &= !(1 << idx); self.bitmap &= !(1 << idx);
} }
true
} }
pub fn next(&mut self) -> Option<(ThreadId, Priority)> { fn pop_level(&mut self, idx: usize) -> Option<(ThreadId, Priority)> {
let thread = self.lists.get(idx)?.head?;
self.remove(thread);
Some((thread, Priority::new(idx as u8)?))
}
pub fn pop_highest(&mut self) -> Option<(ThreadId, Priority)> {
if self.bitmap == 0 { if self.bitmap == 0 {
return None; return None;
} }
// highest set bit = highest priority // highest set bit = highest priority
let idx = 31 - self.bitmap.leading_zeros() as usize; self.pop_level(31 - self.bitmap.leading_zeros() as usize)
let thread = self.queues[idx].pop()?;
if self.queues[idx].is_empty() {
self.bitmap &= !(1 << idx);
}
Some((thread, Priority::new(idx as u8)?))
} }
pub fn highest_priority(&self) -> Option<Priority> { pub fn highest_priority(&self) -> Option<Priority> {
@@ -117,15 +161,16 @@ impl PriorityQueue {
if self.bitmap == 0 { if self.bitmap == 0 {
return None; return None;
} }
let idx = self.bitmap.trailing_zeros() as usize; self.pop_level(self.bitmap.trailing_zeros() as usize)
let thread = self.queues[idx].pop()?;
if self.queues[idx].is_empty() {
self.bitmap &= !(1 << idx);
}
Some((thread, Priority::new(idx as u8)?))
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.bitmap == 0 self.bitmap == 0
} }
} }
impl Default for PriorityQueue {
fn default() -> Self {
Self::new()
}
}

View File

@@ -22,7 +22,7 @@ fn prio(value: u8) -> Priority {
#[test] #[test]
fn empty_queue_returns_none() { fn empty_queue_returns_none() {
let mut q = PriorityQueue::new(); let mut q = PriorityQueue::new();
assert!(q.next().is_none()); assert!(q.pop_highest().is_none());
assert!(q.is_empty()); assert!(q.is_empty());
} }
@@ -31,7 +31,7 @@ fn single_thread() {
let mut q = PriorityQueue::new(); let mut q = PriorityQueue::new();
q.add(thread(1), prio(10)); q.add(thread(1), prio(10));
let (t, p) = q.next().unwrap(); let (t, p) = q.pop_highest().unwrap();
assert_eq!(t, thread(1)); assert_eq!(t, thread(1));
assert_eq!(p, prio(10)); assert_eq!(p, prio(10));
} }
@@ -43,7 +43,7 @@ fn highest_priority_runs_first() {
q.add(thread(2), prio(20)); q.add(thread(2), prio(20));
q.add(thread(3), prio(10)); q.add(thread(3), prio(10));
let (t, _) = q.next().unwrap(); let (t, _) = q.pop_highest().unwrap();
assert_eq!(t, thread(2)); assert_eq!(t, thread(2));
} }
@@ -54,10 +54,10 @@ fn same_priority_is_fifo() {
q.add(thread(2), prio(10)); q.add(thread(2), prio(10));
q.add(thread(3), prio(10)); q.add(thread(3), prio(10));
assert_eq!(q.next().unwrap().0, thread(1)); assert_eq!(q.pop_highest().unwrap().0, thread(1));
assert_eq!(q.next().unwrap().0, thread(2)); assert_eq!(q.pop_highest().unwrap().0, thread(2));
assert_eq!(q.next().unwrap().0, thread(3)); assert_eq!(q.pop_highest().unwrap().0, thread(3));
assert!(q.next().is_none()); assert!(q.pop_highest().is_none());
} }
#[test] #[test]
@@ -66,18 +66,18 @@ fn remove_thread() {
q.add(thread(1), prio(10)); q.add(thread(1), prio(10));
q.add(thread(2), prio(10)); q.add(thread(2), prio(10));
q.remove(thread(1), prio(10)); q.remove(thread(1));
let (t, _) = q.next().unwrap(); let (t, _) = q.pop_highest().unwrap();
assert_eq!(t, thread(2)); assert_eq!(t, thread(2));
assert!(q.next().is_none()); assert!(q.pop_highest().is_none());
} }
#[test] #[test]
fn remove_clears_bitmap_when_empty() { fn remove_clears_bitmap_when_empty() {
let mut q = PriorityQueue::new(); let mut q = PriorityQueue::new();
q.add(thread(1), prio(10)); q.add(thread(1), prio(10));
q.remove(thread(1), prio(10)); q.remove(thread(1));
assert!(q.is_empty()); assert!(q.is_empty());
} }
@@ -89,9 +89,9 @@ fn drain_all_priorities() {
q.add(thread(2), prio(15)); q.add(thread(2), prio(15));
q.add(thread(3), prio(31)); q.add(thread(3), prio(31));
assert_eq!(q.next().unwrap().0, thread(3)); // highest first assert_eq!(q.pop_highest().unwrap().0, thread(3)); // highest first
assert_eq!(q.next().unwrap().0, thread(2)); assert_eq!(q.pop_highest().unwrap().0, thread(2));
assert_eq!(q.next().unwrap().0, thread(1)); // lowest last assert_eq!(q.pop_highest().unwrap().0, thread(1)); // lowest last
assert!(q.is_empty()); assert!(q.is_empty());
} }
@@ -137,7 +137,7 @@ fn highest_priority_peeks_without_popping() {
q.add(thread(1), prio(5)); q.add(thread(1), prio(5));
q.add(thread(2), prio(20)); q.add(thread(2), prio(20));
assert_eq!(q.highest_priority(), Some(prio(20))); assert_eq!(q.highest_priority(), Some(prio(20)));
assert_eq!(q.next().unwrap().0, thread(2)); assert_eq!(q.pop_highest().unwrap().0, thread(2));
} }
#[test] #[test]
@@ -154,7 +154,7 @@ fn steal_lowest_takes_lowest_priority_thread() {
let (t, p) = q.steal_lowest().unwrap(); let (t, p) = q.steal_lowest().unwrap();
assert_eq!(t, thread(1)); assert_eq!(t, thread(1));
assert_eq!(p, prio(5)); assert_eq!(p, prio(5));
assert_eq!(q.next().unwrap().0, thread(2)); assert_eq!(q.pop_highest().unwrap().0, thread(2));
} }
#[test] #[test]
@@ -240,3 +240,89 @@ fn multi_threads_on_different_cores() {
assert_eq!(s.next(core(0)), Some(thread(1))); assert_eq!(s.next(core(0)), Some(thread(1)));
assert_eq!(s.next(core(1)), Some(thread(2))); assert_eq!(s.next(core(1)), Some(thread(2)));
} }
#[test]
fn requeued_thread_goes_behind_same_priority_peers() {
let mut q = PriorityQueue::new();
let p = Priority::new(5).unwrap();
q.add(thread(1), p);
q.add(thread(2), p);
assert_eq!(q.pop_highest().unwrap().0, thread(1));
q.add(thread(1), p); // preempted, back in the run list
assert_eq!(q.pop_highest().unwrap().0, thread(2));
assert_eq!(q.pop_highest().unwrap().0, thread(1));
}
#[test]
fn round_robin_survives_wraparound() {
let mut q = PriorityQueue::new();
let p = Priority::new(5).unwrap();
q.add(thread(1), p);
q.add(thread(2), p);
q.add(thread(3), p);
for _ in 0..40 {
let (a, _) = q.pop_highest().unwrap();
q.add(a, p);
}
// 40 rotations of 3 threads: 40 % 3 == 1, so thread 2 is at the head
assert_eq!(q.pop_highest().unwrap().0, thread(2));
assert_eq!(q.pop_highest().unwrap().0, thread(3));
assert_eq!(q.pop_highest().unwrap().0, thread(1));
}
#[test]
fn removed_thread_is_skipped_and_order_kept() {
let mut q = PriorityQueue::new();
let p = Priority::new(5).unwrap();
q.add(thread(1), p);
q.add(thread(2), p);
q.add(thread(3), p);
q.remove(thread(2));
assert_eq!(q.pop_highest().unwrap().0, thread(1));
assert_eq!(q.pop_highest().unwrap().0, thread(3));
assert!(q.pop_highest().is_none());
}
#[test]
fn run_now_picks_the_named_thread_not_the_queue_head() {
let mut s = MultiCoreScheduler::new(1);
s.add(thread(1), prio(10), core(0));
s.add(thread(2), prio(10), core(0));
s.add(thread(3), prio(10), core(0));
assert!(s.run_now(core(0), thread(2)));
assert_eq!(s.current(core(0)), Some(thread(2)));
// The others keep their order; thread 2 is no longer queued.
assert_eq!(s.next(core(0)), Some(thread(1)));
assert_eq!(s.next(core(0)), Some(thread(3)));
assert_eq!(s.next(core(0)), None);
}
#[test]
fn run_now_rejects_a_thread_that_is_not_queued() {
let mut s = MultiCoreScheduler::new(1);
s.add(thread(1), prio(10), core(0));
assert!(!s.run_now(core(0), thread(9)));
assert_eq!(s.current(core(0)), None);
}
#[test]
fn an_unknown_core_is_ignored_not_a_panic() {
let mut s = MultiCoreScheduler::new(1);
s.add(thread(1), prio(10), core(200));
assert_eq!(s.next(core(200)), None);
assert_eq!(s.current(core(200)), None);
s.set_current(core(200), thread(1));
assert!(!s.run_now(core(200), thread(1)));
}
#[test]
fn an_out_of_range_thread_id_is_ignored_not_a_panic() {
let mut s = MultiCoreScheduler::new(1);
s.add(thread(60000), prio(10), core(0));
s.remove(thread(60000));
assert!(!s.is_queued(thread(60000)));
let _ = s.tick(core(0), thread(60000));
}

View File

@@ -1,43 +1,13 @@
use types::{ use types::{
address::{DataRef, EntryPoint, StackPointer, VirtualAddress}, address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
badge::Badge,
bounded::{CapSlot, PageCount}, bounded::{CapSlot, PageCount},
mapping::PagePerms,
permission::PermissionSet, permission::PermissionSet,
}; };
use crate::message::Message; use crate::message::Message;
pub struct WaitSet {
endpoints: [Option<CapSlot>; 4],
interrupts: [Option<CapSlot>; 4],
}
impl WaitSet {
pub fn new(
endpoints: [Option<CapSlot>; 4],
interrupts: [Option<CapSlot>; 4],
) -> Self {
Self { endpoints, interrupts }
}
pub fn endpoints(&self) -> &[Option<CapSlot>; 4] {
&self.endpoints
}
pub fn interrupts(&self) -> &[Option<CapSlot>; 4] {
&self.interrupts
}
pub fn is_empty(&self) -> bool {
self.endpoints.iter().all(|s| s.is_none())
&& self.interrupts.iter().all(|s| s.is_none())
}
}
pub enum WakeReason {
MessageReady(CapSlot),
InterruptFired(CapSlot),
}
pub enum Syscall { pub enum Syscall {
Send { Send {
endpoint: CapSlot, endpoint: CapSlot,
@@ -51,15 +21,47 @@ pub enum Syscall {
to_endpoint: CapSlot, to_endpoint: CapSlot,
permissions: PermissionSet, permissions: PermissionSet,
}, },
WaitForInterrupt { /// Send, then block for the reply (docs/ipc.md).
Call {
endpoint: CapSlot,
message: Message,
},
/// Answer the thread in the caller's reply slot.
Reply {
message: Message,
},
/// Reply (if a reply is pending), then receive.
ReplyRecv {
endpoint: CapSlot,
message: Message,
},
/// Copy an unbadged endpoint/notification cap with a badge and
/// narrower permissions into the caller's own table.
Mint {
slot: CapSlot,
badge: Badge,
permissions: PermissionSet,
},
CreateNotification,
Signal {
notification: CapSlot,
},
Wait {
notification: CapSlot,
},
Poll {
notification: CapSlot,
},
BindNotification {
notification: CapSlot,
},
SetIrqNotification {
irq: CapSlot, irq: CapSlot,
notification: CapSlot,
}, },
AcknowledgeInterrupt { AcknowledgeInterrupt {
irq: CapSlot, irq: CapSlot,
}, },
WaitAny {
sources: WaitSet,
},
CreateProcess { CreateProcess {
create_cap: CapSlot, create_cap: CapSlot,
image: DataRef, image: DataRef,
@@ -67,9 +69,30 @@ pub enum Syscall {
Kill { Kill {
process: CapSlot, process: CapSlot,
}, },
/// Deletes every capability derived from `slot`, keeping `slot`.
Revoke { Revoke {
slot: CapSlot, slot: CapSlot,
}, },
/// Allocates a shared-memory region charged to the caller and returns
/// a capability to it.
CreateRegion {
pages: PageCount,
},
/// Maps the whole region named by `region` at `va` in the caller.
MapRegion {
region: CapSlot,
va: VirtualAddress,
perms: PagePerms,
},
/// The physical base of the region named by `region`, so a driver can
/// point a device's DMA at memory it has mapped.
RegionAddress {
region: CapSlot,
},
/// Removes a capability from the caller's own table.
DropCap {
slot: CapSlot,
},
SpawnThread { SpawnThread {
entry: EntryPoint, entry: EntryPoint,
stack: StackPointer, stack: StackPointer,
@@ -86,9 +109,14 @@ pub enum Syscall {
GetProcessId, GetProcessId,
Yield, Yield,
Exit, Exit,
Halt, /// Stops the machine; needs a SystemControl capability.
Halt {
control: CapSlot,
},
CreateEndpoint, CreateEndpoint,
DebugPutchar { byte: u8 }, DebugPutchar {
byte: u8,
},
MapPage { MapPage {
process: CapSlot, process: CapSlot,
src_va: VirtualAddress, src_va: VirtualAddress,

View File

@@ -1,10 +1,11 @@
use types::{ use types::{
bounded::{CapSlot, Priority}, bounded::Priority,
id::{CoreId, ProcessId, ThreadId}, generation::Generation,
id::{CoreId, EndpointId, NotificationId, ProcessId, ThreadId},
time::TimeSlice, time::TimeSlice,
}; };
use crate::error::KernelError; use crate::{error::KernelError, generations::SlotGenerations};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
@@ -20,6 +21,7 @@ pub struct Thread {
core_affinity: Option<CoreId>, core_affinity: Option<CoreId>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadState { pub enum ThreadState {
Ready, Ready,
Running(CoreId), Running(CoreId),
@@ -77,11 +79,18 @@ impl SavedContext {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockReason { pub enum BlockReason {
Sending(CapSlot), /// Queued as a sender on the endpoint.
Receiving(CapSlot), Sending(EndpointId),
WaitingForInterrupt(CapSlot), /// Queued as a `Call`er on the endpoint; will then await the reply.
WaitingAny, Calling(EndpointId),
/// Queued as a receiver on the endpoint.
Receiving(EndpointId),
/// Delivered a `Call`; waiting for the server's reply.
AwaitingReply,
/// Blocked in `Wait` on the notification.
WaitingNotification(NotificationId),
} }
impl Thread { impl Thread {
@@ -178,6 +187,17 @@ impl Thread {
} }
} }
/// Keeps a blocked thread blocked, waiting for something else.
pub fn rewait(&mut self, reason: BlockReason) -> Result<(), KernelError> {
match self.state {
ThreadState::Blocked(_) => {
self.state = ThreadState::Blocked(reason);
Ok(())
}
_ => Err(KernelError::InvalidStateTransition(self.id)),
}
}
pub fn unblock(&mut self) -> Result<(), KernelError> { pub fn unblock(&mut self) -> Result<(), KernelError> {
match self.state { match self.state {
ThreadState::Blocked(_) => { ThreadState::Blocked(_) => {
@@ -209,19 +229,35 @@ impl Thread {
} }
} }
pub const MAX_THREADS: usize = 256; pub const MAX_THREADS: usize = if cfg!(kani) { 3 } else { 256 };
pub struct ThreadTable { pub struct ThreadTable {
threads: [Option<Thread>; MAX_THREADS], threads: [Option<Thread>; MAX_THREADS],
generations: SlotGenerations<MAX_THREADS>,
} }
impl ThreadTable { impl ThreadTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
threads: [const { None }; MAX_THREADS], threads: [const { None }; MAX_THREADS],
generations: SlotGenerations::new(),
} }
} }
/// Current generation of a live thread, so references to it (reply
/// slots) can detect that the id was reused.
pub fn generation(&self, id: ThreadId) -> Option<Generation> {
self.get(id).ok()?;
self.generations.current(id.raw() as usize)
}
fn free_slot(&self) -> Option<usize> {
self.threads
.iter()
.enumerate()
.position(|(i, t)| t.is_none() && self.generations.is_usable(i))
}
pub fn spawn( pub fn spawn(
&mut self, &mut self,
process: ProcessId, process: ProcessId,
@@ -229,14 +265,19 @@ impl ThreadTable {
time_slice: TimeSlice, time_slice: TimeSlice,
core_affinity: Option<CoreId>, core_affinity: Option<CoreId>,
) -> Result<ThreadId, KernelError> { ) -> Result<ThreadId, KernelError> {
let idx = self let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
.threads
.iter()
.position(|s| s.is_none())
.ok_or(KernelError::TooManyThreads)?;
let id = ThreadId::from_raw(idx as u16); let id = ThreadId::from_raw(idx as u16);
self.threads[idx] = Some(Thread::new(id, process, priority, time_slice, core_affinity)); *self
.threads
.get_mut(idx)
.ok_or(KernelError::TooManyThreads)? = Some(Thread::new(
id,
process,
priority,
time_slice,
core_affinity,
));
Ok(id) Ok(id)
} }
@@ -248,17 +289,31 @@ impl ThreadTable {
core_affinity: Option<CoreId>, core_affinity: Option<CoreId>,
context: SavedContext, context: SavedContext,
) -> Result<ThreadId, KernelError> { ) -> Result<ThreadId, KernelError> {
let idx = self let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
.threads
.iter()
.position(|s| s.is_none())
.ok_or(KernelError::TooManyThreads)?;
let id = ThreadId::from_raw(idx as u16); let id = ThreadId::from_raw(idx as u16);
self.threads[idx] = Some(Thread::with_context(id, process, priority, time_slice, core_affinity, context)); *self
.threads
.get_mut(idx)
.ok_or(KernelError::TooManyThreads)? = Some(Thread::with_context(
id,
process,
priority,
time_slice,
core_affinity,
context,
));
Ok(id) Ok(id)
} }
/// Every live thread with its id.
pub fn iter(&self) -> impl Iterator<Item = (ThreadId, &Thread)> {
self.threads
.iter()
.enumerate()
.filter_map(|(i, t)| Some((ThreadId::from_raw(i as u16), t.as_ref()?)))
}
pub fn get(&self, id: ThreadId) -> Result<&Thread, KernelError> { pub fn get(&self, id: ThreadId) -> Result<&Thread, KernelError> {
let idx = id.raw() as usize; let idx = id.raw() as usize;
self.threads self.threads
@@ -277,10 +332,13 @@ impl ThreadTable {
pub fn destroy(&mut self, id: ThreadId) -> Result<Thread, KernelError> { pub fn destroy(&mut self, id: ThreadId) -> Result<Thread, KernelError> {
let idx = id.raw() as usize; let idx = id.raw() as usize;
self.threads let thread = self
.threads
.get_mut(idx) .get_mut(idx)
.and_then(|s| s.take()) .and_then(|s| s.take())
.ok_or(KernelError::InvalidThread(id)) .ok_or(KernelError::InvalidThread(id))?;
self.generations.advance(idx);
Ok(thread)
} }
pub fn threads_for_process(&self, process: ProcessId) -> impl Iterator<Item = &Thread> { pub fn threads_for_process(&self, process: ProcessId) -> impl Iterator<Item = &Thread> {
@@ -290,3 +348,9 @@ impl ThreadTable {
.filter(move |t| t.process() == process) .filter(move |t| t.process() == process)
} }
} }
impl Default for ThreadTable {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,6 +1,6 @@
use super::*; use super::*;
use types::bounded::Priority; use types::bounded::Priority;
use types::id::{CoreId, ProcessId, ThreadId}; use types::id::{CoreId, EndpointId, ProcessId, ThreadId};
use types::time::TimeSlice; use types::time::TimeSlice;
fn thread(id: u16) -> ThreadId { fn thread(id: u16) -> ThreadId {
@@ -65,15 +65,18 @@ fn ready_to_running() {
fn running_to_blocked() { fn running_to_blocked() {
let mut t = test_thread(0, 0); let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap(); t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap(); t.block(BlockReason::AwaitingReply).unwrap();
assert!(matches!(t.state(), ThreadState::Blocked(BlockReason::WaitingAny))); assert!(matches!(
t.state(),
ThreadState::Blocked(BlockReason::AwaitingReply)
));
} }
#[test] #[test]
fn blocked_to_ready() { fn blocked_to_ready() {
let mut t = test_thread(0, 0); let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap(); t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap(); t.block(BlockReason::AwaitingReply).unwrap();
t.unblock().unwrap(); t.unblock().unwrap();
assert!(matches!(t.state(), ThreadState::Ready)); assert!(matches!(t.state(), ThreadState::Ready));
} }
@@ -102,7 +105,7 @@ fn dead_cannot_transition() {
t.set_running(core(0)).unwrap(); t.set_running(core(0)).unwrap();
t.exit().unwrap(); t.exit().unwrap();
assert!(t.set_running(core(0)).is_err()); assert!(t.set_running(core(0)).is_err());
assert!(t.block(BlockReason::WaitingAny).is_err()); assert!(t.block(BlockReason::AwaitingReply).is_err());
assert!(t.unblock().is_err()); assert!(t.unblock().is_err());
assert!(t.preempt().is_err()); assert!(t.preempt().is_err());
assert!(t.exit().is_err()); assert!(t.exit().is_err());
@@ -111,14 +114,14 @@ fn dead_cannot_transition() {
#[test] #[test]
fn ready_cannot_block() { fn ready_cannot_block() {
let mut t = test_thread(0, 0); let mut t = test_thread(0, 0);
assert!(t.block(BlockReason::WaitingAny).is_err()); assert!(t.block(BlockReason::AwaitingReply).is_err());
} }
#[test] #[test]
fn blocked_cannot_run_directly() { fn blocked_cannot_run_directly() {
let mut t = test_thread(0, 0); let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap(); t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap(); t.block(BlockReason::AwaitingReply).unwrap();
assert!(t.set_running(core(0)).is_err()); assert!(t.set_running(core(0)).is_err());
} }
@@ -138,7 +141,7 @@ fn ready_cannot_exit() {
fn blocked_cannot_preempt() { fn blocked_cannot_preempt() {
let mut t = test_thread(0, 0); let mut t = test_thread(0, 0);
t.set_running(core(0)).unwrap(); t.set_running(core(0)).unwrap();
t.block(BlockReason::WaitingAny).unwrap(); t.block(BlockReason::AwaitingReply).unwrap();
assert!(t.preempt().is_err()); assert!(t.preempt().is_err());
} }
@@ -162,7 +165,9 @@ fn time_slice_mut_ticks() {
#[test] #[test]
fn table_spawn_and_lookup() { fn table_spawn_and_lookup() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
let id = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let id = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
let t = table.get(id).unwrap(); let t = table.get(id).unwrap();
assert_eq!(t.id(), id); assert_eq!(t.id(), id);
assert_eq!(t.process(), process(0)); assert_eq!(t.process(), process(0));
@@ -171,15 +176,21 @@ fn table_spawn_and_lookup() {
#[test] #[test]
fn table_spawn_returns_unique_ids() { fn table_spawn_returns_unique_ids() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
let a = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let a = table
let b = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); .spawn(process(0), prio(10), time_slice(), None)
.unwrap();
let b = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
assert_ne!(a, b); assert_ne!(a, b);
} }
#[test] #[test]
fn table_destroy_then_get_fails() { fn table_destroy_then_get_fails() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
let id = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let id = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
table.destroy(id).unwrap(); table.destroy(id).unwrap();
assert!(table.get(id).is_err()); assert!(table.get(id).is_err());
} }
@@ -187,9 +198,15 @@ fn table_destroy_then_get_fails() {
#[test] #[test]
fn table_threads_for_process_filters() { fn table_threads_for_process_filters() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
table.spawn(process(0), prio(10), time_slice(), None).unwrap(); table
table.spawn(process(1), prio(10), time_slice(), None).unwrap(); .spawn(process(0), prio(10), time_slice(), None)
table.spawn(process(0), prio(5), time_slice(), None).unwrap(); .unwrap();
table
.spawn(process(1), prio(10), time_slice(), None)
.unwrap();
table
.spawn(process(0), prio(5), time_slice(), None)
.unwrap();
let count = table.threads_for_process(process(0)).count(); let count = table.threads_for_process(process(0)).count();
assert_eq!(count, 2); assert_eq!(count, 2);
@@ -198,16 +215,54 @@ fn table_threads_for_process_filters() {
#[test] #[test]
fn table_get_mut_allows_state_transition() { fn table_get_mut_allows_state_transition() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
let id = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let id = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
table.get_mut(id).unwrap().set_running(core(0)).unwrap(); table.get_mut(id).unwrap().set_running(core(0)).unwrap();
assert!(matches!(table.get(id).unwrap().state(), ThreadState::Running(_))); assert!(matches!(
table.get(id).unwrap().state(),
ThreadState::Running(_)
));
} }
#[test] #[test]
fn table_destroy_reuses_slot() { fn table_destroy_reuses_slot() {
let mut table = ThreadTable::new(); let mut table = ThreadTable::new();
let id = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let id = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
table.destroy(id).unwrap(); table.destroy(id).unwrap();
let new_id = table.spawn(process(0), prio(10), time_slice(), None).unwrap(); let new_id = table
.spawn(process(0), prio(10), time_slice(), None)
.unwrap();
assert_eq!(id, new_id); assert_eq!(id, new_id);
} }
#[test]
fn a_blocked_thread_can_change_what_it_waits_for() {
let mut t = Thread::new(
ThreadId::from_raw(1),
ProcessId::from_raw(0),
Priority::DEFAULT,
TimeSlice::DEFAULT,
None,
);
t.set_running(CoreId::from_raw(0)).unwrap();
t.block(BlockReason::Calling(EndpointId::from_raw(0)))
.unwrap();
t.rewait(BlockReason::AwaitingReply).unwrap();
assert_eq!(*t.state(), ThreadState::Blocked(BlockReason::AwaitingReply));
}
#[test]
fn only_blocked_threads_can_rewait() {
let mut t = Thread::new(
ThreadId::from_raw(1),
ProcessId::from_raw(0),
Priority::DEFAULT,
TimeSlice::DEFAULT,
None,
);
assert!(t.rewait(BlockReason::AwaitingReply).is_err());
assert_eq!(*t.state(), ThreadState::Ready);
}

View File

@@ -134,7 +134,7 @@ impl ElfInfo {
pub fn total_pages(&self) -> usize { pub fn total_pages(&self) -> usize {
let mut total = 0; let mut total = 0;
for seg in self.segments() { for seg in self.segments() {
total += (seg.mem_size + 4095) / 4096; total += seg.mem_size.div_ceil(4096);
} }
total total
} }

View File

@@ -87,9 +87,11 @@ pub const MMIO_FIRST: usize = 0x10001000;
pub const MMIO_STRIDE: usize = 0x1000; pub const MMIO_STRIDE: usize = 0x1000;
pub const MMIO_SLOTS: usize = 8; pub const MMIO_SLOTS: usize = 8;
pub fn scan_for_device(device_id: u32) -> Option<DirectMmio> { /// Probes the MMIO slots starting at `first`, the address the caller sees
/// slot 0 at (`MMIO_FIRST` itself, or wherever it is mapped).
pub fn scan_for_device(first: usize, device_id: u32) -> Option<DirectMmio> {
for slot in 0..MMIO_SLOTS { for slot in 0..MMIO_SLOTS {
if let Some(mmio) = probe_slot(MMIO_FIRST + slot * MMIO_STRIDE, device_id) { if let Some(mmio) = probe_slot(first + slot * MMIO_STRIDE, device_id) {
return Some(mmio); return Some(mmio);
} }
} }

View File

@@ -7,3 +7,6 @@ edition = "2024"
types = { workspace = true } types = { workspace = true }
domain = { workspace = true } domain = { workspace = true }
platform = { workspace = true } platform = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }

162
crates/kernel/src/abi.rs Normal file
View File

@@ -0,0 +1,162 @@
//! The syscall register ABI (docs/ipc.md), as pure functions so it can be
//! tested and proved: registers in, `Syscall` out; result out, registers
//! in. Architecture adapters only move registers in and out of a frame.
use domain::{message::Message, syscall::Syscall};
use types::{
address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
badge::Badge,
bounded::{CapSlot, PageCount},
permission::PermissionSet,
syscall_abi::*,
};
use crate::syscall::{SyscallResult, error_code};
fn slot(r: usize) -> Option<CapSlot> {
reg_slot(r)?
}
fn pages(r: usize) -> Option<PageCount> {
PageCount::new(u16::try_from(r).ok()?)
}
fn permissions(r: usize) -> Option<PermissionSet> {
Some(PermissionSet::from_bits(u8::try_from(r).ok()?))
}
/// The message in `a1..a5`.
fn message(a: &[usize; 6]) -> Option<Message> {
let msg = Message::from_words([a[1], a[2], a[3], a[4]]);
Some(match reg_slot(a[5])? {
None => msg,
Some(cap) => msg.with_cap(cap),
})
}
/// Decodes syscall `nr` with arguments `a0..a5`. `None` for an unknown
/// number or an argument with no meaning (e.g. a slot out of range); the
/// caller then gets `UnknownSyscall` and nothing else happens.
pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
Some(match nr {
SYS_EXIT => Syscall::Exit,
SYS_HALT => Syscall::Halt {
control: slot(a[0])?,
},
SYS_EXIT_THREAD => Syscall::ExitThread,
SYS_YIELD => Syscall::Yield,
SYS_GET_TIME => Syscall::GetTime,
SYS_GET_PROCESS_ID => Syscall::GetProcessId,
SYS_SEND => Syscall::Send {
endpoint: slot(a[0])?,
message: message(a)?,
},
SYS_CALL => Syscall::Call {
endpoint: slot(a[0])?,
message: message(a)?,
},
SYS_RECEIVE => Syscall::Receive {
endpoint: slot(a[0])?,
},
SYS_REPLY => Syscall::Reply {
message: message(a)?,
},
SYS_REPLY_RECV => Syscall::ReplyRecv {
endpoint: slot(a[0])?,
message: message(a)?,
},
SYS_DELEGATE => Syscall::Delegate {
slot: slot(a[0])?,
to_endpoint: slot(a[1])?,
permissions: permissions(a[2])?,
},
SYS_MINT => Syscall::Mint {
slot: slot(a[0])?,
badge: Badge::new(a[1]),
permissions: permissions(a[2])?,
},
SYS_CREATE_NOTIFICATION => Syscall::CreateNotification,
SYS_SIGNAL => Syscall::Signal {
notification: slot(a[0])?,
},
SYS_WAIT => Syscall::Wait {
notification: slot(a[0])?,
},
SYS_POLL => Syscall::Poll {
notification: slot(a[0])?,
},
SYS_BIND_NOTIFICATION => Syscall::BindNotification {
notification: slot(a[0])?,
},
SYS_SET_IRQ_NOTIFICATION => Syscall::SetIrqNotification {
irq: slot(a[0])?,
notification: slot(a[1])?,
},
SYS_ACKNOWLEDGE_INTERRUPT => Syscall::AcknowledgeInterrupt { irq: slot(a[0])? },
SYS_CREATE_PROCESS => Syscall::CreateProcess {
create_cap: slot(a[0])?,
image: DataRef::empty(),
},
SYS_KILL => Syscall::Kill {
process: slot(a[0])?,
},
SYS_REVOKE => Syscall::Revoke { slot: slot(a[0])? },
SYS_DROP_CAP => Syscall::DropCap { slot: slot(a[0])? },
SYS_SPAWN_THREAD => Syscall::SpawnThread {
entry: EntryPoint::from_raw(a[0]),
stack: StackPointer::from_raw(a[1]),
},
SYS_REQUEST_PAGES => Syscall::RequestPages {
count: pages(a[0])?,
},
SYS_RELEASE_PAGES => Syscall::ReleasePages {
addr: VirtualAddress::from_raw(a[0]),
count: pages(a[1])?,
},
SYS_SPAWN_THREAD_IN => Syscall::SpawnThreadIn {
process: slot(a[0])?,
entry: EntryPoint::from_raw(a[1]),
stack: StackPointer::from_raw(a[2]),
},
SYS_CREATE_ENDPOINT => Syscall::CreateEndpoint,
SYS_CREATE_REGION => Syscall::CreateRegion {
pages: pages(a[0])?,
},
SYS_MAP_REGION => Syscall::MapRegion {
region: slot(a[0])?,
va: VirtualAddress::from_raw(a[1]),
perms: reg_perms(a[2])?,
},
SYS_REGION_ADDRESS => Syscall::RegionAddress {
region: slot(a[0])?,
},
SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a[0] as u8 },
SYS_MAP_PAGE => Syscall::MapPage {
process: slot(a[0])?,
src_va: VirtualAddress::from_raw(a[1]),
dst_va: VirtualAddress::from_raw(a[2]),
executable: a[3] != 0,
},
_ => return None,
})
}
/// Result registers `a0..a7` for a completed syscall.
pub fn encode(result: &SyscallResult) -> [usize; 8] {
let mut r = [0usize; 8];
match result {
SyscallResult::Ok => {}
SyscallResult::Value(v) => r[1] = *v,
SyscallResult::Slot(s) => r[1] = s.value() as usize,
SyscallResult::Received(d) => r = delivery_regs(d),
SyscallResult::Error(e) => r[0] = error_code(e) as usize,
}
r
}
/// Registers for a syscall number that does not decode.
pub fn unknown_syscall() -> [usize; 8] {
let mut r = [0usize; 8];
r[0] = ErrorCode::UnknownSyscall as usize;
r
}

View File

@@ -0,0 +1,79 @@
//! The authority init starts with. The board describes its devices; the
//! kernel turns each into capabilities (an MMIO region and an IRQ line)
//! and adds one SystemControl capability. Init then decides who gets what;
//! nothing else is privileged by its process id.
use domain::{error::KernelError, resource::Resource};
use platform::Platform;
use types::{
address::PhysicalAddress,
bounded::PageCount,
id::{IrqId, ProcessId, RegionId},
permission::{Permission, PermissionSet},
};
use crate::Kernel;
/// A device the board exposes to userspace: its MMIO window and, if it
/// raises interrupts, its IRQ line.
#[derive(Debug, Clone, Copy)]
pub struct BoardDevice {
pub base: PhysicalAddress,
pub pages: PageCount,
pub irq: Option<IrqId>,
}
impl<P: Platform> Kernel<P> {
/// Creates one device region per board device, owned by `init`. Fails
/// on the first device that is misaligned, overlaps the frame pool or
/// another device, or names an IRQ the kernel cannot route.
pub fn register_board(
&mut self,
init: ProcessId,
devices: &[BoardDevice],
) -> Result<(), KernelError> {
for device in devices {
if let Some(irq) = device.irq
&& !self.irqs.is_valid(irq)
{
return Err(KernelError::InvalidIrq(irq));
}
self.create_device_region(init, device.base, device.pages)?;
}
Ok(())
}
/// Gives `init` a capability to every registered device's region and
/// IRQ line, then one SystemControl capability. All carry Delegate so
/// init can hand them on. Done separately from `register_board` so
/// boot code can wire drivers to the regions first.
pub fn grant_boot_authority(
&mut self,
init: ProcessId,
devices: &[BoardDevice],
) -> Result<(), KernelError> {
let delegate = PermissionSet::empty().with(Permission::Delegate);
let control = delegate.with(Permission::Execute);
let mmio = delegate.with(Permission::Read).with(Permission::Write);
for device in devices {
let region = self
.device_region_at(device.base)
.filter(|id| self.regions.get(*id).is_some_and(|r| r.owner() == init))
.ok_or(KernelError::NoSuchObject)?;
self.grant_cap(init, Resource::Memory(region), mmio)?;
if let Some(irq) = device.irq {
self.grant_cap(init, Resource::Interrupt(irq), control)?;
}
}
self.grant_cap(init, Resource::SystemControl, control)?;
Ok(())
}
/// The device region that starts at `base`.
pub fn device_region_at(&self, base: PhysicalAddress) -> Option<RegionId> {
self.regions
.iter()
.find(|(_, r)| r.backing() == domain::memory::RegionBacking::Device && r.base() == base)
.map(|(id, _)| id)
}
}

View File

@@ -0,0 +1,172 @@
//! The capability derivation tree. Every derived capability (Mint,
//! Delegate, IPC transfer) records its parent; `Revoke` deletes a subtree
//! and deleting a capability re-parents its children, so the tree can only
//! shrink by explicit deletion, never by a middleman stepping out.
use domain::{
capability::{CapRef, Capability, MAX_CAPS_PER_PROCESS, derivation::descendants},
error::KernelError,
process::MAX_PROCESSES,
};
use platform::Platform;
use types::{
bounded::CapSlot,
id::{ProcessId, RegionId},
};
use crate::Kernel;
/// Every capability slot in the system, flattened.
const ALL_CAPS: usize = MAX_PROCESSES * MAX_CAPS_PER_PROCESS;
fn flat(pid: ProcessId, slot: CapSlot) -> usize {
pid.raw() as usize * MAX_CAPS_PER_PROCESS + slot.value() as usize
}
fn unflat(i: usize) -> Option<(ProcessId, CapSlot)> {
let pid = ProcessId::from_raw((i / MAX_CAPS_PER_PROCESS) as u16);
let slot = CapSlot::new((i % MAX_CAPS_PER_PROCESS) as u8)?;
Some((pid, slot))
}
impl<P: Platform> Kernel<P> {
/// A reference to the capability currently in `slot` of `pid`.
pub(crate) fn cap_ref(&self, pid: ProcessId, slot: CapSlot) -> Option<CapRef> {
let process = self.processes.get(pid).ok()?;
Some(CapRef {
process: pid,
process_generation: self.processes.generation(pid)?,
slot,
slot_generation: process.capabilities().slot_generation(slot)?,
})
}
/// Whether `r` still names a live capability.
pub(crate) fn resolves(&self, r: CapRef) -> bool {
self.cap_ref(r.process, r.slot) == Some(r)
}
/// Stores `cap` in `to`'s table as a child of `parent`.
pub(crate) fn insert_derived(
&mut self,
to: ProcessId,
cap: Capability,
parent: Option<CapRef>,
) -> Result<CapSlot, KernelError> {
self.processes
.get_mut(to)?
.capabilities_mut()
.insert(cap.with_parent(parent))
}
/// Calls `f` with every live capability's flat index and reference.
fn for_each_cap(&self, mut f: impl FnMut(usize, CapRef, &Capability)) {
for p in 0..MAX_PROCESSES {
let pid = ProcessId::from_raw(p as u16);
let Ok(process) = self.processes.get(pid) else {
continue;
};
for (slot, cap) in process.capabilities().iter() {
if let Some(r) = self.cap_ref(pid, slot) {
f(flat(pid, slot), r, cap);
}
}
}
}
/// Removes every region mapping made through `via`.
fn unmap_mappings_via(&mut self, via: CapRef) {
let mut doomed: [Option<RegionId>; domain::memory::MAX_REGIONS] =
[None; domain::memory::MAX_REGIONS];
for (slot, (id, _)) in doomed.iter_mut().zip(
self.regions
.iter()
.filter(|(_, r)| r.mappings().any(|m| m.via == Some(via))),
) {
*slot = Some(id);
}
for id in doomed.into_iter().flatten() {
self.unmap_region_from(id, via.process);
}
}
/// Removes one capability: its mappings go, its children are adopted
/// by its parent.
pub(crate) fn delete_cap(&mut self, pid: ProcessId, slot: CapSlot) -> Result<(), KernelError> {
let me = self
.cap_ref(pid, slot)
.ok_or(KernelError::InvalidSlot(slot))?;
let grandparent = self.processes.get(pid)?.capabilities().get(slot)?.parent();
self.unmap_mappings_via(me);
let mut children = [false; ALL_CAPS];
self.for_each_cap(|i, _, cap| {
if cap.parent() == Some(me)
&& let Some(c) = children.get_mut(i)
{
*c = true;
}
});
for (i, _) in children.iter().enumerate().filter(|(_, c)| **c) {
if let Some((cpid, cslot)) = unflat(i)
&& let Ok(p) = self.processes.get_mut(cpid)
&& let Ok(child) = p.capabilities_mut().get_mut(cslot)
{
child.set_parent(grandparent);
}
}
self.processes.get_mut(pid)?.capabilities_mut().remove(slot)
}
/// Deletes every capability derived from `slot` of `pid`, transitively,
/// and keeps `slot` itself.
pub(crate) fn revoke_subtree(
&mut self,
pid: ProcessId,
slot: CapSlot,
) -> Result<(), KernelError> {
if self.cap_ref(pid, slot).is_none() {
return Err(KernelError::InvalidSlot(slot));
}
// Snapshot the tree as parent indices. A parent that no longer
// resolves cannot exist (deletion re-parents), but is treated as
// a root rather than trusted.
let mut parents: [Option<u16>; ALL_CAPS] = [None; ALL_CAPS];
self.for_each_cap(|i, _, cap| {
if let Some(p) = parents.get_mut(i) {
*p = cap
.parent()
.filter(|r| self.resolves(*r))
.map(|r| flat(r.process, r.slot) as u16);
}
});
let root = flat(pid, slot);
let marked = descendants(&parents, root);
for (i, _) in marked.iter().enumerate().filter(|(i, m)| **m && *i != root) {
if let Some((dpid, dslot)) = unflat(i)
&& let Some(r) = self.cap_ref(dpid, dslot)
{
self.unmap_mappings_via(r);
let _ = self
.processes
.get_mut(dpid)
.map(|p| p.capabilities_mut().remove(dslot));
}
}
Ok(())
}
/// Deletes all of a dying process's capabilities, re-parenting their
/// children so revocation still reaches them.
pub(crate) fn delete_all_caps(&mut self, pid: ProcessId) {
let mut slots = [None; MAX_CAPS_PER_PROCESS];
if let Ok(p) = self.processes.get(pid) {
for (out, (slot, _)) in slots.iter_mut().zip(p.capabilities().iter()) {
*out = Some(slot);
}
}
for slot in slots.into_iter().flatten() {
let _ = self.delete_cap(pid, slot);
}
}
}

View File

@@ -1,6 +1,6 @@
use domain::scheduler::{Scheduler, SchedulerDecision}; use domain::scheduler::{Scheduler, SchedulerDecision};
use platform::Platform; use platform::Platform;
use types::id::{CoreId, IrqId, ThreadId}; use types::id::CoreId;
use crate::Kernel; use crate::Kernel;
@@ -26,15 +26,26 @@ impl<P: Platform> Kernel<P> {
self.scheduler.add(current_tid, priority, core); self.scheduler.add(current_tid, priority, core);
match self.scheduler.next(core) { match self.scheduler.next(core) {
Some(next) => { Some(next) => {
if self.threads.get_mut(next).and_then(|t| t.set_running(core)).is_ok() { if self
.threads
.get_mut(next)
.and_then(|t| t.set_running(core))
.is_ok()
{
SchedulerDecision::Preempt(next) SchedulerDecision::Preempt(next)
} else { } else {
let _ = self.threads.get_mut(current_tid).map(|t| t.set_running(core)); let _ = self
.threads
.get_mut(current_tid)
.map(|t| t.set_running(core));
SchedulerDecision::Continue SchedulerDecision::Continue
} }
} }
None => { None => {
let _ = self.threads.get_mut(current_tid).map(|t| t.set_running(core)); let _ = self
.threads
.get_mut(current_tid)
.map(|t| t.set_running(core));
SchedulerDecision::Continue SchedulerDecision::Continue
} }
} }
@@ -42,22 +53,4 @@ impl<P: Platform> Kernel<P> {
self.scheduler.tick(core, current_tid) self.scheduler.tick(core, current_tid)
} }
} }
pub fn handle_interrupt(&mut self, irq: IrqId) -> Option<ThreadId> {
self.platform.mask_irq(irq);
let tid = self.irqs.owner(irq)?;
let thread = self.threads.get_mut(tid).ok()?;
if !matches!(thread.state(), domain::thread::ThreadState::Blocked(_)) {
return None;
}
let priority = thread.priority();
let core = self.platform.core_id();
let _ = thread.unblock();
self.scheduler.add(tid, priority, core);
Some(tid)
}
} }

View File

@@ -1,10 +1,30 @@
#![no_std] #![no_std]
// A kernel panic takes the whole system down, so kernel code may not
// contain any way to panic. Tests and proofs are exempt.
#![cfg_attr(
not(any(test, kani)),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing
)
)]
pub mod abi;
mod board;
mod captree;
pub mod interrupt; pub mod interrupt;
mod memory;
#[cfg(kani)] #[cfg(kani)]
pub mod proofs; pub mod proofs;
mod smp;
pub mod syscall; pub mod syscall;
pub use board::BoardDevice;
pub use syscall::{SyscallOutcome, SyscallResult}; pub use syscall::{SyscallOutcome, SyscallResult};
#[cfg(test)] #[cfg(test)]
@@ -16,20 +36,16 @@ use domain::{
endpoint::EndpointTable, endpoint::EndpointTable,
error::KernelError, error::KernelError,
irq::IrqRoutingTable, irq::IrqRoutingTable,
memory::{ memory::{RegionTable, frame::FrameTable, quota::Quota},
page_pool::PagePool, notification::{MAX_NOTIFICATIONS, NotificationTable},
process_memory::ProcessMemory, process::{MAX_PROCESSES, ProcessTable},
RegionTable,
},
message::Message,
process::{ProcessTable, MAX_PROCESSES},
resource::Resource, resource::Resource,
scheduler::{multi_core::MultiCoreScheduler, Scheduler}, scheduler::{Scheduler, multi_core::MultiCoreScheduler},
syscall::WaitSet, thread::{MAX_THREADS, ThreadState, ThreadTable},
thread::{ThreadTable, ThreadState, MAX_THREADS},
}; };
use platform::Platform; use platform::Platform;
use types::{ use types::{
address::PhysicalAddress,
bounded::{CapSlot, Priority}, bounded::{CapSlot, Priority},
generation::Generation, generation::Generation,
id::{ProcessId, ThreadId}, id::{ProcessId, ThreadId},
@@ -43,17 +59,19 @@ pub struct Kernel<P: Platform> {
threads: ThreadTable, threads: ThreadTable,
endpoints: EndpointTable, endpoints: EndpointTable,
scheduler: MultiCoreScheduler, scheduler: MultiCoreScheduler,
page_pool: PagePool, frames: FrameTable,
process_memory: [Option<ProcessMemory>; MAX_PROCESSES], quotas: [Quota; MAX_PROCESSES],
regions: RegionTable, regions: RegionTable,
irqs: IrqRoutingTable, irqs: IrqRoutingTable,
pending_messages: [Option<Message>; MAX_THREADS], notifications: NotificationTable,
ipc: [syscall::IpcState; MAX_THREADS],
pending_results: [Option<SyscallResult>; MAX_THREADS], pending_results: [Option<SyscallResult>; MAX_THREADS],
wait_sets: [Option<WaitSet>; MAX_THREADS], /// The core each thread runs on (see smp.rs).
homes: [types::id::CoreId; MAX_THREADS],
} }
impl<P: Platform> Kernel<P> { impl<P: Platform> Kernel<P> {
pub fn new(platform: P, page_pool: PagePool) -> Self { pub fn new(platform: P, pool_base: PhysicalAddress, pool_pages: usize) -> Self {
let num_cores = platform.num_cores(); let num_cores = platform.num_cores();
Self { Self {
platform, platform,
@@ -61,13 +79,47 @@ impl<P: Platform> Kernel<P> {
threads: ThreadTable::new(), threads: ThreadTable::new(),
endpoints: EndpointTable::new(), endpoints: EndpointTable::new(),
scheduler: MultiCoreScheduler::new(num_cores), scheduler: MultiCoreScheduler::new(num_cores),
page_pool, frames: FrameTable::new(pool_base, pool_pages),
process_memory: [const { None }; MAX_PROCESSES], quotas: [Quota::default(); MAX_PROCESSES],
regions: RegionTable::new(), regions: RegionTable::new(),
irqs: IrqRoutingTable::new(), irqs: IrqRoutingTable::new(),
pending_messages: [const { None }; MAX_THREADS], notifications: NotificationTable::new(),
ipc: [syscall::IpcState::default(); MAX_THREADS],
pending_results: [const { None }; MAX_THREADS], pending_results: [const { None }; MAX_THREADS],
wait_sets: [const { None }; MAX_THREADS], homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
}
}
/// Builds the kernel directly in `slot`, one table at a time, so the
/// whole structure never has to fit on the (small) boot stack.
///
/// # Safety
/// `slot` must be valid for writes and properly aligned. Any previous
/// value is overwritten without being dropped.
pub unsafe fn init_in_place(
slot: *mut Self,
platform: P,
pool_base: PhysicalAddress,
pool_pages: usize,
) {
use core::ptr::addr_of_mut;
let num_cores = platform.num_cores();
// SAFETY: the caller guarantees `slot` is valid and aligned; each
// field is written exactly once.
unsafe {
addr_of_mut!((*slot).platform).write(platform);
addr_of_mut!((*slot).processes).write(ProcessTable::new());
addr_of_mut!((*slot).threads).write(ThreadTable::new());
addr_of_mut!((*slot).endpoints).write(EndpointTable::new());
addr_of_mut!((*slot).scheduler).write(MultiCoreScheduler::new(num_cores));
addr_of_mut!((*slot).frames).write(FrameTable::new(pool_base, pool_pages));
addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]);
addr_of_mut!((*slot).regions).write(RegionTable::new());
addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new());
addr_of_mut!((*slot).notifications).write(NotificationTable::new());
addr_of_mut!((*slot).ipc).write([syscall::IpcState::default(); MAX_THREADS]);
addr_of_mut!((*slot).pending_results).write([const { None }; MAX_THREADS]);
addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]);
} }
} }
@@ -77,20 +129,25 @@ impl<P: Platform> Kernel<P> {
time_slice: TimeSlice, time_slice: TimeSlice,
) -> Result<(ProcessId, ThreadId), KernelError> { ) -> Result<(ProcessId, ThreadId), KernelError> {
let pid = self.processes.create(CapabilityTable::new(), None)?; let pid = self.processes.create(CapabilityTable::new(), None)?;
if let Err(e) = self.create_address_space(pid) {
let _ = self.processes.destroy(pid);
return Err(e);
}
let tid = self.threads.spawn(pid, priority, time_slice, None)?; let tid = self.threads.spawn(pid, priority, time_slice, None)?;
self.processes.get_mut(pid)?.add_thread(tid)?; self.processes.get_mut(pid)?.add_thread(tid)?;
self.process_memory[pid.raw() as usize] =
Some(ProcessMemory::new(pid, ProcessMemory::DEFAULT_PAGE_LIMIT));
let cap = Capability::new( let cap = Capability::new(
Resource::ProcessCreate, Resource::ProcessCreate,
PermissionSet::empty().with(Permission::Execute), PermissionSet::empty().with(Permission::Execute),
Generation::initial(), Generation::initial(),
); );
self.processes.get_mut(pid)?.capabilities_mut().insert(cap)?; self.processes
.get_mut(pid)?
.capabilities_mut()
.insert(cap)?;
let core = self.platform.core_id(); let core = self.platform.core_id();
self.set_home(tid, core);
self.scheduler.add(tid, priority, core); self.scheduler.add(tid, priority, core);
self.scheduler.next(core); self.scheduler.next(core);
self.threads.get_mut(tid)?.set_running(core)?; self.threads.get_mut(tid)?.set_running(core)?;
@@ -102,21 +159,21 @@ impl<P: Platform> Kernel<P> {
} }
pub fn pending_result(&self, thread: ThreadId) -> Option<&SyscallResult> { pub fn pending_result(&self, thread: ThreadId) -> Option<&SyscallResult> {
self.pending_results[thread.raw() as usize].as_ref() self.pending_results.get(thread.raw() as usize)?.as_ref()
} }
pub fn take_pending_result(&mut self, thread: ThreadId) -> Option<SyscallResult> { pub fn take_pending_result(&mut self, thread: ThreadId) -> Option<SyscallResult> {
self.pending_results[thread.raw() as usize].take() self.pending_results.get_mut(thread.raw() as usize)?.take()
} }
pub fn set_thread_running(&mut self, tid: ThreadId, core: types::id::CoreId) -> Result<(), domain::error::KernelError> { pub fn set_thread_running(
&mut self,
tid: ThreadId,
core: types::id::CoreId,
) -> Result<(), domain::error::KernelError> {
self.threads.get_mut(tid)?.set_running(core) self.threads.get_mut(tid)?.set_running(core)
} }
pub fn free_page_count(&self) -> usize {
self.page_pool.free_count()
}
pub fn process_exists(&self, pid: ProcessId) -> bool { pub fn process_exists(&self, pid: ProcessId) -> bool {
self.processes.get(pid).is_ok() self.processes.get(pid).is_ok()
} }
@@ -125,8 +182,14 @@ impl<P: Platform> Kernel<P> {
self.threads.get(tid).ok().map(|t| t.saved_context()) self.threads.get(tid).ok().map(|t| t.saved_context())
} }
pub fn thread_context_mut(&mut self, tid: ThreadId) -> Option<&mut domain::thread::SavedContext> { pub fn thread_context_mut(
self.threads.get_mut(tid).ok().map(|t| t.saved_context_mut()) &mut self,
tid: ThreadId,
) -> Option<&mut domain::thread::SavedContext> {
self.threads
.get_mut(tid)
.ok()
.map(|t| t.saved_context_mut())
} }
pub fn thread_process(&self, tid: ThreadId) -> Option<ProcessId> { pub fn thread_process(&self, tid: ThreadId) -> Option<ProcessId> {
@@ -146,30 +209,31 @@ impl<P: Platform> Kernel<P> {
pub fn create_process_internal(&mut self) -> Option<ProcessId> { pub fn create_process_internal(&mut self) -> Option<ProcessId> {
let pid = self.processes.create(CapabilityTable::new(), None).ok()?; let pid = self.processes.create(CapabilityTable::new(), None).ok()?;
self.process_memory[pid.raw() as usize] = if self.create_address_space(pid).is_err() {
Some(domain::memory::process_memory::ProcessMemory::new(pid, ProcessMemory::DEFAULT_PAGE_LIMIT)); let _ = self.processes.destroy(pid);
return None;
}
Some(pid) Some(pid)
} }
pub fn spawn_thread_internal( pub fn spawn_thread_internal(&mut self, pid: ProcessId) -> Option<ThreadId> {
&mut self, let tid = self
pid: ProcessId, .threads
) -> Option<ThreadId> { .spawn(pid, Priority::DEFAULT, TimeSlice::DEFAULT, None)
use domain::scheduler::Scheduler; .ok()?;
let tid = self.threads.spawn(
pid,
Priority::DEFAULT,
TimeSlice::DEFAULT,
None,
).ok()?;
let _ = self.processes.get_mut(pid).map(|p| p.add_thread(tid)); let _ = self.processes.get_mut(pid).map(|p| p.add_thread(tid));
let core = self.platform.core_id(); self.place(tid);
self.scheduler.add(tid, Priority::DEFAULT, core); self.make_ready(tid, Priority::DEFAULT);
Some(tid) Some(tid)
} }
pub fn create_endpoint(&mut self) -> Result<types::id::EndpointId, KernelError> { /// Creates an endpoint owned by `owner`; it is destroyed with `owner`.
self.endpoints.create().ok_or(KernelError::NoFreeSlot) pub fn create_endpoint(
&mut self,
owner: ProcessId,
) -> Result<types::id::EndpointId, KernelError> {
self.processes.get(owner)?;
self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot)
} }
pub fn grant_capability( pub fn grant_capability(
@@ -181,34 +245,11 @@ impl<P: Platform> Kernel<P> {
self.grant_cap(to, resource, permissions) self.grant_cap(to, resource, permissions)
} }
pub fn switch_to_thread(&mut self, tid: ThreadId) {
use domain::scheduler::Scheduler;
let core = self.platform.core_id();
if let Some(current) = self.scheduler.current(core) {
if current == tid {
return;
}
if let Ok(t) = self.threads.get_mut(current) {
if matches!(t.state(), ThreadState::Running(_)) {
let priority = t.priority();
let _ = t.preempt();
self.scheduler.add(current, priority, core);
}
}
}
if let Ok(t) = self.threads.get(tid) {
if matches!(t.state(), ThreadState::Ready) {
self.scheduler.next(core);
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
}
}
}
fn caller(&self) -> Result<(ThreadId, ProcessId), KernelError> { fn caller(&self) -> Result<(ThreadId, ProcessId), KernelError> {
let core = self.platform.core_id(); let core = self.platform.core_id();
let tid = self.scheduler.current(core) let tid = self
.scheduler
.current(core)
.ok_or(KernelError::WouldBlock)?; .ok_or(KernelError::WouldBlock)?;
let thread = self.threads.get(tid)?; let thread = self.threads.get(tid)?;
match thread.state() { match thread.state() {
@@ -220,11 +261,12 @@ impl<P: Platform> Kernel<P> {
pub(crate) fn destroy_process(&mut self, pid: ProcessId) { pub(crate) fn destroy_process(&mut self, pid: ProcessId) {
use domain::process::MAX_THREADS_PER_PROCESS; use domain::process::MAX_THREADS_PER_PROCESS;
let thread_ids: [Option<ThreadId>; MAX_THREADS_PER_PROCESS] = match self.processes.get(pid) { let thread_ids: [Option<ThreadId>; MAX_THREADS_PER_PROCESS] = match self.processes.get(pid)
{
Ok(p) => { Ok(p) => {
let mut ids = [None; MAX_THREADS_PER_PROCESS]; let mut ids = [None; MAX_THREADS_PER_PROCESS];
for (i, tid) in p.thread_ids().enumerate() { for (slot, tid) in ids.iter_mut().zip(p.thread_ids()) {
if i < MAX_THREADS_PER_PROCESS { ids[i] = Some(tid); } *slot = Some(tid);
} }
ids ids
} }
@@ -232,50 +274,78 @@ impl<P: Platform> Kernel<P> {
}; };
self.cleanup_threads(&thread_ids); self.cleanup_threads(&thread_ids);
self.delete_all_caps(pid);
// Free allocated pages back to pool self.destroy_endpoints_owned_by(pid);
if let Some(pm) = &self.process_memory[pid.raw() as usize] { self.destroy_notifications_owned_by(pid);
for &(pa, count) in pm.allocations() { self.teardown_memory(pid);
self.page_pool.free(pa, count);
}
}
// Unblock threads in other processes that were blocked waiting
// on endpoints where this process's threads were the other side.
// The cleanup_threads call above already removed this process's
// threads from all endpoint wait queues. Threads blocked waiting
// for this process will eventually time out or be rescheduled
// when the endpoint is revoked.
let _ = self.processes.destroy(pid); let _ = self.processes.destroy(pid);
self.process_memory[pid.raw() as usize] = None;
} }
pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) { pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) {
use domain::scheduler::Scheduler; use domain::scheduler::Scheduler;
for slot in thread_ids { for tid in thread_ids.iter().flatten() {
if let Some(tid) = slot { self.endpoints.remove_thread_from_all(*tid);
self.endpoints.remove_thread_from_all(*tid); self.notifications.remove_thread_from_all(*tid);
self.cleanup_wait_any(*tid); let ipc = self.ipc_mut(*tid).map(core::mem::take).unwrap_or_default();
self.scheduler.remove(*tid); if let Some(caller) = ipc.reply_to {
self.pending_messages[tid.raw() as usize] = None; self.release_caller(caller);
self.pending_results[tid.raw() as usize] = None; }
let _ = self.threads.destroy(*tid); self.evict(*tid);
self.scheduler.remove(*tid);
self.set_pending_result(*tid, None);
let _ = self.threads.destroy(*tid);
}
}
/// Destroys every endpoint `owner` created, waking anyone blocked on
/// them with an error instead of leaving them blocked forever.
pub(crate) fn destroy_endpoints_owned_by(&mut self, owner: ProcessId) {
let mut owned = [None; domain::endpoint::MAX_ENDPOINTS];
for (slot, id) in owned.iter_mut().zip(self.endpoints.ids_owned_by(owner)) {
*slot = Some(id);
}
for id in owned.into_iter().flatten() {
let Some(mut ep) = self.endpoints.destroy(id) else {
continue;
};
while let Some(tid) = ep.dequeue_sender().or_else(|| ep.dequeue_receiver()) {
self.wake_with_error(tid, KernelError::InvalidEndpoint(id));
} }
} }
} }
pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) { /// Destroys every notification `owner` created, waking its waiters
if let Some(wait_set) = self.wait_sets[tid.raw() as usize].take() { /// with an error and unbinding its bound thread.
for ep_slot in wait_set.endpoints() { pub(crate) fn destroy_notifications_owned_by(&mut self, owner: ProcessId) {
if let Some(_slot) = ep_slot { let mut owned = [None; MAX_NOTIFICATIONS];
self.endpoints.remove_thread_from_all(tid); for (slot, id) in owned.iter_mut().zip(self.notifications.ids_owned_by(owner)) {
break; *slot = Some(id);
} }
for id in owned.into_iter().flatten() {
let Some(mut n) = self.notifications.destroy(id) else {
continue;
};
if let Some(bound) = n.bound()
&& let Some(s) = self.ipc_mut(bound)
{
s.bound = None;
}
while let Some(tid) = n.pop_waiter() {
self.wake_with_error(tid, KernelError::NoSuchObject);
} }
} }
} }
/// Makes a blocked thread runnable with `error` as its syscall result,
/// dropping whatever it was about to send.
fn wake_with_error(&mut self, tid: ThreadId, error: KernelError) {
if let Some(s) = self.ipc_mut(tid) {
s.outgoing = None;
}
self.wake(tid, SyscallResult::Error(error));
}
fn check_cap( fn check_cap(
&self, &self,
owner: ProcessId, owner: ProcessId,
@@ -287,42 +357,23 @@ impl<P: Platform> Kernel<P> {
if !cap.permissions().allows(needed) { if !cap.permissions().allows(needed) {
return Err(KernelError::PermissionDenied { slot, needed }); return Err(KernelError::PermissionDenied { slot, needed });
} }
let current_gen = self.resource_generation(cap.resource()); match self.resource_generation(cap.resource()) {
if !cap.generation().is_valid_against(&current_gen) { Some(current) if cap.generation().is_valid_against(&current) => Ok(cap),
return Err(KernelError::RevokedCapability(slot)); _ => Err(KernelError::RevokedCapability(slot)),
}
Ok(cap)
}
fn resource_generation(&self, resource: &Resource) -> Generation {
match resource {
Resource::Endpoint(id) => self
.endpoints
.get(*id)
.map(|ep| *ep.generation())
.unwrap_or_else(|| Generation::initial()),
Resource::Memory(id) => self
.regions
.get(*id)
.map(|r| *r.generation())
.unwrap_or_else(|| Generation::initial()),
_ => Generation::initial(),
} }
} }
pub fn revoke_resource(&mut self, resource: &Resource) { /// The generation a capability to `resource` must carry, or `None` if
/// the object no longer exists.
fn resource_generation(&self, resource: &Resource) -> Option<Generation> {
match resource { match resource {
Resource::Endpoint(id) => { Resource::Endpoint(id) => self.endpoints.generation(*id),
if let Some(ep) = self.endpoints.get_mut(*id) { Resource::Memory(id) => self.regions.generation(*id),
ep.revoke(); Resource::Notification(id) => self.notifications.generation(*id),
} Resource::Process(pid) => self.processes.generation(*pid),
Resource::Interrupt(_) | Resource::ProcessCreate | Resource::SystemControl => {
Some(Generation::initial())
} }
Resource::Memory(id) => {
if let Some(r) = self.regions.get_mut(*id) {
r.revoke();
}
}
_ => {}
} }
} }
@@ -332,7 +383,9 @@ impl<P: Platform> Kernel<P> {
resource: Resource, resource: Resource,
permissions: PermissionSet, permissions: PermissionSet,
) -> Result<CapSlot, KernelError> { ) -> Result<CapSlot, KernelError> {
let generation = self.resource_generation(&resource); let generation = self
.resource_generation(&resource)
.ok_or(KernelError::NoSuchObject)?;
let cap = Capability::new(resource, permissions, generation); let cap = Capability::new(resource, permissions, generation);
self.processes.get_mut(to)?.capabilities_mut().insert(cap) self.processes.get_mut(to)?.capabilities_mut().insert(cap)
} }

486
crates/kernel/src/memory.rs Normal file
View File

@@ -0,0 +1,486 @@
//! Address-space bookkeeping. The kernel decides every user mapping from
//! its own records (`FrameTable`, `RegionTable`, quotas) and only then tells
//! the platform to apply it; it never asks the hardware what is mapped.
use domain::{
error::KernelError,
memory::{
Region, RegionBacking, RegionMapping,
frame::{FrameTable, FrameUse, PAGE_SIZE},
quota::Quota,
},
};
use platform::{FrameSource, MapError, Platform};
use types::{
address::{PhysicalAddress, VirtualAddress},
bounded::PageCount,
id::{ProcessId, RegionId},
mapping::PagePerms,
};
use crate::Kernel;
/// First VA the kernel considers when picking a spot for `RequestPages`.
/// Below it sit the program image and boot-time device windows.
pub(crate) const USER_HEAP_START: usize = 0x40000;
/// Page-table frames for one address space, charged to its owner.
struct ChargedFrames<'a> {
frames: &'a mut FrameTable,
quota: &'a mut Quota,
owner: ProcessId,
}
impl FrameSource for ChargedFrames<'_> {
fn alloc_table_frame(&mut self) -> Option<PhysicalAddress> {
if !self.quota.charge(1) {
return None;
}
match self.frames.alloc(FrameUse::PageTable { owner: self.owner }) {
Some(pa) => Some(pa),
None => {
self.quota.uncharge(1);
None
}
}
}
}
fn map_error(e: MapError) -> KernelError {
match e {
MapError::OutOfFrames => KernelError::OutOfMemory,
MapError::InvalidAddress => KernelError::InvalidAddress,
MapError::NoAddressSpace => KernelError::OutOfMemory,
}
}
impl<P: Platform> Kernel<P> {
/// Whether `[va, va + pages)` is page aligned and inside the user window.
pub(crate) fn in_user_window(va: VirtualAddress, pages: usize) -> bool {
let start = va.raw();
let Some(len) = pages.checked_mul(PAGE_SIZE) else {
return false;
};
let Some(end) = start.checked_add(len) else {
return false;
};
pages > 0
&& start.is_multiple_of(PAGE_SIZE)
&& start >= P::USER_VA_START
&& end <= P::USER_VA_END
}
fn is_mapped(&self, pid: ProcessId, va: VirtualAddress) -> bool {
self.frames.find_user(pid, va).is_some() || self.regions.covering(pid, va).is_some()
}
fn range_is_free(&self, pid: ProcessId, va: VirtualAddress, pages: usize) -> bool {
(0..pages).all(|i| !self.is_mapped(pid, page_at(va, i)))
}
fn quota_mut(&mut self, pid: ProcessId) -> Result<&mut Quota, KernelError> {
self.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))
}
pub fn page_usage(&self, pid: ProcessId) -> usize {
self.quotas.get(pid.raw() as usize).map_or(0, Quota::used)
}
pub fn set_page_limit(&mut self, pid: ProcessId, limit: usize) -> Result<(), KernelError> {
self.processes.get(pid)?;
if self.quota_mut(pid)?.set_limit(limit) {
Ok(())
} else {
Err(KernelError::OutOfMemory)
}
}
pub fn free_page_count(&self) -> usize {
self.frames.free_count()
}
/// Maps `pa` at `va` in `pid`, building page tables on `pid`'s quota.
fn platform_map(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
let quota = self
.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))?;
let mut source = ChargedFrames {
frames: &mut self.frames,
quota,
owner: pid,
};
self.platform
.map_page(pid, va, pa, perms, &mut source)
.map_err(map_error)
}
pub(crate) fn create_address_space(&mut self, pid: ProcessId) -> Result<(), KernelError> {
let quota = self
.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))?;
*quota = Quota::default();
let mut source = ChargedFrames {
frames: &mut self.frames,
quota,
owner: pid,
};
let created = self
.platform
.create_address_space(pid, &mut source)
.map_err(map_error);
if created.is_err() {
self.frames.free_owned_by(pid);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
}
created
}
/// Allocates, zeroes and maps one fresh page for `pid` at `va`.
fn map_fresh_page(
&mut self,
pid: ProcessId,
va: VirtualAddress,
perms: PagePerms,
) -> Result<PhysicalAddress, KernelError> {
let usage = FrameUse::user(pid, va, perms).ok_or(KernelError::InvalidAddress)?;
if !self.quota_mut(pid)?.charge(1) {
return Err(KernelError::OutOfMemory);
}
let Some(pa) = self.frames.alloc(usage) else {
self.quota_mut(pid)?.uncharge(1);
return Err(KernelError::OutOfMemory);
};
self.platform.zero_frame(pa);
if let Err(e) = self.platform_map(pid, va, pa, perms) {
self.frames.free(pa);
self.quota_mut(pid)?.uncharge(1);
return Err(e);
}
Ok(pa)
}
/// Removes one user page of `pid` and returns its frame to the pool.
fn unmap_and_free(&mut self, pid: ProcessId, va: VirtualAddress) {
if let Some((pa, _)) = self.frames.find_user(pid, va) {
self.platform.unmap_page(pid, va);
self.frames.free(pa);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
q.uncharge(1);
}
}
}
/// Frame backing the user page at `va` in `pid`. Lets boot code fill
/// pages it has just mapped for a process.
pub fn user_page_frame(&self, pid: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
self.frames.find_user(pid, va).map(|(pa, _)| pa)
}
/// Maps `pages` fresh zeroed pages at `va`, all or nothing.
pub fn map_fresh_range(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pages: usize,
perms: PagePerms,
) -> Result<(), KernelError> {
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !self.range_is_free(pid, va, pages) {
return Err(KernelError::AddressInUse);
}
for i in 0..pages {
if let Err(e) = self.map_fresh_page(pid, page_at(va, i), perms) {
for j in 0..i {
self.unmap_and_free(pid, page_at(va, j));
}
return Err(e);
}
}
Ok(())
}
/// First-fit search for `pages` free pages in the heap part of the window.
pub(crate) fn find_free_heap_range(
&self,
pid: ProcessId,
pages: usize,
) -> Option<VirtualAddress> {
let mut candidate = USER_HEAP_START.max(P::USER_VA_START);
while Self::in_user_window(VirtualAddress::from_raw(candidate), pages) {
let va = VirtualAddress::from_raw(candidate);
match (0..pages).find(|&i| self.is_mapped(pid, page_at(va, i))) {
None => return Some(va),
Some(busy) => candidate += (busy + 1) * PAGE_SIZE,
}
}
None
}
/// Validates that every page of the range is a user page `pid` owns,
/// then releases them all.
pub(crate) fn release_range(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pages: usize,
) -> Result<(), KernelError> {
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !(0..pages).all(|i| self.frames.find_user(pid, page_at(va, i)).is_some()) {
return Err(KernelError::InvalidAddress);
}
for i in 0..pages {
self.unmap_and_free(pid, page_at(va, i));
}
Ok(())
}
/// Moves the page at `src` in `from` to `dst` in `to`. The frame and its
/// quota charge change hands; `from` loses access.
pub(crate) fn move_page(
&mut self,
from: ProcessId,
src: VirtualAddress,
to: ProcessId,
dst: VirtualAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
if !Self::in_user_window(src, 1) || !Self::in_user_window(dst, 1) {
return Err(KernelError::InvalidAddress);
}
let (pa, _) = self
.frames
.find_user(from, src)
.ok_or(KernelError::InvalidAddress)?;
self.processes.get(to)?;
if !self.range_is_free(to, dst, 1) {
return Err(KernelError::AddressInUse);
}
let usage = FrameUse::user(to, dst, perms).ok_or(KernelError::InvalidAddress)?;
if !self.quota_mut(to)?.charge(1) {
return Err(KernelError::OutOfMemory);
}
if let Err(e) = self.platform_map(to, dst, pa, perms) {
self.quota_mut(to)?.uncharge(1);
return Err(e);
}
self.platform.unmap_page(from, src);
self.frames.retag(pa, usage);
self.quota_mut(from)?.uncharge(1);
Ok(())
}
/// Allocates a physically contiguous, zeroed region charged to `owner`.
pub fn create_pool_region(
&mut self,
owner: ProcessId,
pages: PageCount,
) -> Result<RegionId, KernelError> {
self.processes.get(owner)?;
let id = self.regions.next_free_id().ok_or(KernelError::NoFreeSlot)?;
let count = pages.value() as usize;
if !self.quota_mut(owner)?.charge(count) {
return Err(KernelError::OutOfMemory);
}
let Some(base) = self
.frames
.alloc_contiguous(count, |_| FrameUse::Region { region: id })
else {
self.quota_mut(owner)?.uncharge(count);
return Err(KernelError::OutOfMemory);
};
for i in 0..count {
self.platform.zero_frame(base.offset(i));
}
self.regions
.insert(Region::new(owner, RegionBacking::Pool, base, pages))
.ok_or(KernelError::NoFreeSlot)
}
/// Describes device memory at `base` so it can be mapped like a region.
pub fn create_device_region(
&mut self,
owner: ProcessId,
base: PhysicalAddress,
pages: PageCount,
) -> Result<RegionId, KernelError> {
self.processes.get(owner)?;
if !base.raw().is_multiple_of(PAGE_SIZE) {
return Err(KernelError::InvalidAddress);
}
// Device memory must never alias frames the kernel hands out.
let len = pages.value() as usize * PAGE_SIZE;
let start = base.raw();
let end = start.checked_add(len).ok_or(KernelError::InvalidAddress)?;
let pool_start = self.frames.base().raw();
let pool_end = pool_start + self.frames.capacity() * PAGE_SIZE;
if start < pool_end && end > pool_start {
return Err(KernelError::InvalidAddress);
}
// Nor another device's window: each device has one region, so
// revoking or delegating it means the same thing everywhere.
let overlaps = self.regions.iter().any(|(_, r)| {
let r_start = r.base().raw();
let r_end = r_start.saturating_add(r.pages().value() as usize * PAGE_SIZE);
r.backing() == RegionBacking::Device && start < r_end && end > r_start
});
if overlaps {
return Err(KernelError::AddressInUse);
}
self.regions
.insert(Region::new(owner, RegionBacking::Device, base, pages))
.ok_or(KernelError::NoFreeSlot)
}
pub fn region_base(&self, id: RegionId) -> Option<PhysicalAddress> {
self.regions.get(id).map(Region::base)
}
pub fn region_pages(&self, id: RegionId) -> Option<PageCount> {
self.regions.get(id).map(Region::pages)
}
/// Maps all of region `id` at `va` in `pid`, all or nothing.
/// Maps region `id` for boot code; the mapping is not tied to any
/// capability and lasts until the region or the process dies.
pub fn map_region(
&mut self,
id: RegionId,
pid: ProcessId,
va: VirtualAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
self.map_region_via(id, pid, va, perms, None)
}
/// Maps all of region `id` at `va` in `pid`, all or nothing. A mapping
/// made through a capability (`via`) is removed when that capability
/// is deleted.
pub(crate) fn map_region_via(
&mut self,
id: RegionId,
pid: ProcessId,
va: VirtualAddress,
perms: PagePerms,
via: Option<domain::capability::CapRef>,
) -> Result<(), KernelError> {
self.processes.get(pid)?;
let region = self.regions.get(id).ok_or(KernelError::InvalidRegion)?;
let (base, pages) = (region.base(), region.pages().value() as usize);
if region.mappings().any(|m| m.process == pid) {
return Err(KernelError::AddressInUse);
}
// W^X holds across processes too: nobody may write what someone
// else can execute.
let clashes = |other: PagePerms| {
matches!(
(perms, other),
(PagePerms::ReadWrite, PagePerms::ReadExecute)
| (PagePerms::ReadExecute, PagePerms::ReadWrite)
)
};
if region.mappings().any(|m| clashes(m.perms)) {
return Err(KernelError::WritableAndExecutable);
}
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !self.range_is_free(pid, va, pages) {
return Err(KernelError::AddressInUse);
}
for i in 0..pages {
if let Err(e) = self.platform_map(pid, page_at(va, i), base.offset(i), perms) {
for j in 0..i {
self.platform.unmap_page(pid, page_at(va, j));
}
return Err(e);
}
}
let recorded = self.regions.get_mut(id).is_some_and(|r| {
r.add_mapping(RegionMapping {
process: pid,
va,
perms,
via,
})
});
if !recorded {
for i in 0..pages {
self.platform.unmap_page(pid, page_at(va, i));
}
return Err(KernelError::NoFreeSlot);
}
Ok(())
}
/// Removes `pid`'s mapping of region `id`, if it has one.
pub(crate) fn unmap_region_from(&mut self, id: RegionId, pid: ProcessId) {
let Some(region) = self.regions.get_mut(id) else {
return;
};
let pages = region.pages().value() as usize;
if let Some(m) = region.remove_mapping(pid) {
for i in 0..pages {
self.platform.unmap_page(pid, page_at(m.va, i));
}
}
}
/// Frees regions owned by `pid` (unmapping them from every other
/// process) and forgets `pid`'s mappings of other regions.
fn teardown_regions(&mut self, pid: ProcessId) {
let mut owned = [None; domain::memory::MAX_REGIONS];
for (slot, id) in owned.iter_mut().zip(self.regions.ids_owned_by(pid)) {
*slot = Some(id);
}
for id in owned.into_iter().flatten() {
let Some(region) = self.regions.remove(id) else {
continue;
};
let pages = region.pages().value() as usize;
for m in region.mappings().filter(|m| m.process != pid) {
for i in 0..pages {
self.platform.unmap_page(m.process, page_at(m.va, i));
}
}
if region.backing() == RegionBacking::Pool {
for i in 0..pages {
self.frames.free(region.base().offset(i));
}
}
}
for (_, region) in self.regions.iter_mut() {
region.remove_mapping(pid);
}
}
/// Releases everything `pid` holds in memory: its address space, page
/// tables, user pages and owned regions.
pub(crate) fn teardown_memory(&mut self, pid: ProcessId) {
self.teardown_regions(pid);
self.platform.destroy_address_space(pid);
self.frames.free_owned_by(pid);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
}
}
fn page_at(base: VirtualAddress, i: usize) -> VirtualAddress {
VirtualAddress::from_raw(base.raw() + i * PAGE_SIZE)
}

View File

@@ -0,0 +1,75 @@
use crate::abi::{decode, encode};
use crate::syscall::SyscallResult;
use domain::error::KernelError;
use domain::syscall::Syscall;
use types::bounded::CapSlot;
use types::syscall_abi::*;
/// No register contents can make decoding panic or overflow.
#[kani::proof]
fn decode_is_total() {
let nr: usize = kani::any();
let args: [usize; 6] = kani::any();
let _ = decode(nr, &args);
}
/// A decoded slot argument is exactly the register it came from.
#[kani::proof]
fn decoded_slots_match_their_registers() {
let args: [usize; 6] = kani::any();
if let Some(Syscall::Receive { endpoint }) = decode(SYS_RECEIVE, &args) {
assert!(endpoint.value() as usize == args[0]);
}
if let Some(Syscall::Delegate {
slot, to_endpoint, ..
}) = decode(SYS_DELEGATE, &args)
{
assert!(slot.value() as usize == args[0]);
assert!(to_endpoint.value() as usize == args[1]);
}
}
/// What libkos encodes for a Send is decoded back to the same message.
#[kani::proof]
fn send_roundtrips() {
let ep = CapSlot::new(kani::any::<u8>() % (CapSlot::MAX + 1)).unwrap();
let words: [usize; 4] = kani::any();
let cap = if kani::any() {
CapSlot::new(kani::any::<u8>() % (CapSlot::MAX + 1))
} else {
None
};
let regs = message_regs(ep.value() as usize, words, cap);
match decode(SYS_SEND, &regs) {
Some(Syscall::Send { endpoint, message }) => {
assert!(endpoint == ep);
assert!(*message.words() == words);
assert!(message.cap() == cap);
}
_ => panic!("valid send did not decode"),
}
}
/// A failed syscall always reports a real, non-zero error code.
#[kani::proof]
fn errors_encode_to_valid_codes() {
let e = match kani::any::<u8>() % 6 {
0 => KernelError::NoReply,
1 => KernelError::OutOfMemory,
2 => KernelError::InvalidAddress,
3 => KernelError::WrongObject,
4 => KernelError::NoSuchObject,
_ => KernelError::InvalidSlot(CapSlot::new(0).unwrap()),
};
let regs = encode(&SyscallResult::Error(e));
assert!(regs[0] != 0);
assert!(ErrorCode::from_raw(regs[0]).is_some());
}
#[kani::proof]
fn successes_report_status_zero() {
let v: usize = kani::any();
assert!(encode(&SyscallResult::Ok)[0] == 0);
let regs = encode(&SyscallResult::Value(v));
assert!(regs[0] == 0 && regs[1] == v);
}

View File

@@ -0,0 +1,118 @@
//! Mint creates no authority. The real kernel, with tables shrunk under
//! `cfg(kani)`, runs `mint` with arbitrary arguments in a small state
//! whose capabilities carry arbitrary rights. Afterwards every capability
//! to an object that already existed carries no more rights than some
//! capability to that object carried before.
//!
//! Only this harness is kept. The same check over delegate, delete,
//! revoke, create and kill either exceeded 12 GB in CBMC or tripped
//! modelling artifacts ("unreachable code" on safe `Option` matches that
//! an exhaustive host replay never reaches). Those operations are covered
//! by the syscall storm's global invariants (tests/storm.rs) and the
//! capability-tree model test (tests/captree_model.rs) instead.
use super::platform::KaniPlatform;
use crate::Kernel;
use domain::{capability::MAX_CAPS_PER_PROCESS, process::MAX_PROCESSES, resource::Resource};
use types::{
address::PhysicalAddress,
badge::Badge,
bounded::{CapSlot, Priority},
id::ProcessId,
permission::PermissionSet,
time::TimeSlice,
};
#[derive(Clone, Copy, PartialEq)]
struct Held {
resource: Resource,
perms: PermissionSet,
badge: Badge,
}
type Snapshot = [[Option<Held>; MAX_CAPS_PER_PROCESS]; MAX_PROCESSES];
fn snapshot(k: &Kernel<KaniPlatform>) -> Snapshot {
let mut s = [[None; MAX_CAPS_PER_PROCESS]; MAX_PROCESSES];
for (p, row) in s.iter_mut().enumerate() {
if let Ok(process) = k.processes.get(ProcessId::from_raw(p as u16)) {
for (slot, cap) in process.capabilities().iter() {
row[slot.value() as usize] = Some(Held {
resource: *cap.resource(),
perms: *cap.permissions(),
badge: cap.badge(),
});
}
}
}
s
}
/// Whether `resource` named a live object before the operation.
fn existed(before: &Snapshot, resource: &Resource) -> bool {
before
.iter()
.flatten()
.flatten()
.any(|h| h.resource == *resource)
}
/// Some capability before the operation already granted at least `h`.
fn justified(before: &Snapshot, h: &Held) -> bool {
before.iter().flatten().flatten().any(|b| {
b.resource == h.resource
&& h.perms.is_subset_of(&b.perms)
&& (b.badge == h.badge || b.badge.is_none())
})
}
fn any_perms() -> PermissionSet {
PermissionSet::from_bits(kani::any::<u8>() & 0b1111)
}
fn any_slot() -> CapSlot {
CapSlot::new(kani::any::<u8>() % (CapSlot::MAX + 1)).unwrap()
}
/// Init and one more process, an endpoint, a notification, and caps with
/// arbitrary rights spread over both, including a cap to the other process.
fn small_world() -> Kernel<KaniPlatform> {
let mut k = Kernel::new(KaniPlatform, PhysicalAddress::from_raw(0x8400_0000), 6);
let (init, _) = k.boot(Priority::DEFAULT, TimeSlice::DEFAULT).unwrap();
let b = k.create_process_internal().unwrap();
k.spawn_thread_internal(b).unwrap();
let ep = k.create_endpoint(init).unwrap();
let n = k.create_notification(b).unwrap();
k.grant_capability(init, Resource::Endpoint(ep), any_perms())
.unwrap();
k.grant_capability(init, Resource::Process(b), any_perms())
.unwrap();
k.grant_capability(b, Resource::Endpoint(ep), any_perms())
.unwrap();
k.grant_capability(b, Resource::Notification(n), any_perms())
.unwrap();
k
}
/// The caller is concrete: a symbolic pid turns every table lookup into a
/// symbolic index and blows CBMC's formula up past 30 GB.
#[kani::proof]
#[kani::unwind(8)]
fn mint_creates_no_authority() {
let mut k = small_world();
let (slot, badge, perms) = (any_slot(), Badge::new(kani::any()), any_perms());
let before = snapshot(&k);
let _ = k.mint(ProcessId::from_raw(0), slot, badge, perms);
let after = snapshot(&k);
for (p, row) in after.iter().enumerate() {
for (s, held) in row.iter().enumerate() {
let Some(h) = held else { continue };
if before[p][s] == Some(*h) {
continue; // untouched
}
if existed(&before, &h.resource) {
assert!(justified(&before, h), "authority appeared from nowhere");
}
}
}
}

View File

@@ -1,6 +1,6 @@
use super::{arbitrary_endpoint_cap, insert_cap};
use domain::capability::{Capability, CapabilityTable}; use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource; use domain::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::generation::Generation; use types::generation::Generation;
use types::id::EndpointId; use types::id::EndpointId;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
@@ -16,17 +16,16 @@ fn kernel_delegate_path_never_escalates() {
kani::assume(source_perms.allows(Permission::Delegate)); kani::assume(source_perms.allows(Permission::Delegate));
let mut source_table = CapabilityTable::new(); let mut source_table = CapabilityTable::new();
let slot = insert_cap(&mut source_table, arbitrary_endpoint_cap(source_bits, ep_id)); let slot = insert_cap(
&mut source_table,
arbitrary_endpoint_cap(source_bits, ep_id),
);
let cap = source_table.get(slot).unwrap(); let cap = source_table.get(slot).unwrap();
let original_perms = *cap.permissions(); let original_perms = *cap.permissions();
let restricted = original_perms.restrict_to(&PermissionSet::from_bits(restrict_bits)); let restricted = original_perms.restrict_to(&PermissionSet::from_bits(restrict_bits));
let delegated = Capability::new( let delegated = Capability::new(*cap.resource(), restricted, Generation::initial());
cap.resource().clone(),
restricted,
Generation::initial(),
);
let mut target_table = CapabilityTable::new(); let mut target_table = CapabilityTable::new();
let target_slot = insert_cap(&mut target_table, delegated); let target_slot = insert_cap(&mut target_table, delegated);
@@ -34,11 +33,18 @@ fn kernel_delegate_path_never_escalates() {
let result = target_table.get(target_slot).unwrap(); let result = target_table.get(target_slot).unwrap();
assert!(result.permissions().is_subset_of(&original_perms)); assert!(result.permissions().is_subset_of(&original_perms));
if !original_perms.allows(Permission::Read) { assert!(!result.permissions().allows(Permission::Read)); } if !original_perms.allows(Permission::Read) {
if !original_perms.allows(Permission::Write) { assert!(!result.permissions().allows(Permission::Write)); } assert!(!result.permissions().allows(Permission::Read));
if !original_perms.allows(Permission::Execute) { assert!(!result.permissions().allows(Permission::Execute)); } }
if !original_perms.allows(Permission::Delegate) { assert!(!result.permissions().allows(Permission::Delegate)); } if !original_perms.allows(Permission::Write) {
if !original_perms.allows(Permission::Revoke) { assert!(!result.permissions().allows(Permission::Revoke)); } assert!(!result.permissions().allows(Permission::Write));
}
if !original_perms.allows(Permission::Execute) {
assert!(!result.permissions().allows(Permission::Execute));
}
if !original_perms.allows(Permission::Delegate) {
assert!(!result.permissions().allows(Permission::Delegate));
}
} }
#[kani::proof] #[kani::proof]
@@ -58,15 +64,19 @@ fn kernel_chained_delegate_never_escalates() {
let slot_a = insert_cap(&mut table_a, arbitrary_endpoint_cap(original_bits, ep_id)); let slot_a = insert_cap(&mut table_a, arbitrary_endpoint_cap(original_bits, ep_id));
let cap = table_a.get(slot_a).unwrap(); let cap = table_a.get(slot_a).unwrap();
let perms_for_b = cap.permissions().restrict_to(&PermissionSet::from_bits(restrict1_bits)); let perms_for_b = cap
.permissions()
.restrict_to(&PermissionSet::from_bits(restrict1_bits));
let mut table_b = CapabilityTable::new(); let mut table_b = CapabilityTable::new();
let cap_b = Capability::new(resource.clone(), perms_for_b, Generation::initial()); let cap_b = Capability::new(resource, perms_for_b, Generation::initial());
let slot_b = insert_cap(&mut table_b, cap_b); let slot_b = insert_cap(&mut table_b, cap_b);
let cap_in_b = table_b.get(slot_b).unwrap(); let cap_in_b = table_b.get(slot_b).unwrap();
if cap_in_b.permissions().allows(Permission::Delegate) { if cap_in_b.permissions().allows(Permission::Delegate) {
let perms_for_c = cap_in_b.permissions().restrict_to(&PermissionSet::from_bits(restrict2_bits)); let perms_for_c = cap_in_b
.permissions()
.restrict_to(&PermissionSet::from_bits(restrict2_bits));
let mut table_c = CapabilityTable::new(); let mut table_c = CapabilityTable::new();
let cap_c = Capability::new(resource, perms_for_c, Generation::initial()); let cap_c = Capability::new(resource, perms_for_c, Generation::initial());

View File

@@ -1,5 +1,5 @@
use domain::capability::CapabilityTable;
use super::{arbitrary_endpoint_cap, insert_cap}; use super::{arbitrary_endpoint_cap, insert_cap};
use domain::capability::CapabilityTable;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
@@ -38,7 +38,7 @@ fn handle_receive_rejects_without_read_permission() {
#[kani::proof] #[kani::proof]
fn missing_cap_always_rejected() { fn missing_cap_always_rejected() {
let slot_val: u8 = kani::any(); let slot_val: u8 = kani::any();
kani::assume(slot_val <= 63); kani::assume(slot_val <= CapSlot::MAX);
let slot = CapSlot::new(slot_val).unwrap(); let slot = CapSlot::new(slot_val).unwrap();
let table = CapabilityTable::new(); let table = CapabilityTable::new();
@@ -60,7 +60,10 @@ fn check_cap_faithfully_reflects_stored_permissions() {
assert!(retrieved.permissions().allows(Permission::Read) == perms.allows(Permission::Read)); assert!(retrieved.permissions().allows(Permission::Read) == perms.allows(Permission::Read));
assert!(retrieved.permissions().allows(Permission::Write) == perms.allows(Permission::Write)); assert!(retrieved.permissions().allows(Permission::Write) == perms.allows(Permission::Write));
assert!(retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute)); assert!(
assert!(retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate)); retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute)
assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke)); );
assert!(
retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate)
);
} }

View File

@@ -1,6 +1,9 @@
mod abi;
mod authority;
mod delegation; mod delegation;
mod mediation; mod mediation;
mod message; mod message;
mod platform;
mod revocation; mod revocation;
use domain::capability::{Capability, CapabilityTable}; use domain::capability::{Capability, CapabilityTable};

View File

@@ -0,0 +1,62 @@
//! A platform with no hardware behind it, for running the real kernel
//! under Kani. Page tables cost one frame per address space.
use platform::{FrameSource, MapError, Platform};
use types::{
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
id::{CoreId, IrqId, ProcessId},
mapping::PagePerms,
time::{Ticks, TimerDuration},
};
pub struct KaniPlatform;
impl Platform for KaniPlatform {
const USER_VA_START: usize = 0x1000;
const USER_VA_END: usize = 0x0200_0000;
fn create_address_space(
&mut self,
_process: ProcessId,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
frames.alloc_table_frame().ok_or(MapError::OutOfFrames)?;
Ok(())
}
fn destroy_address_space(&mut self, _process: ProcessId) {}
fn map_page(
&mut self,
_process: ProcessId,
_va: VirtualAddress,
_pa: PhysicalAddress,
_perms: PagePerms,
_frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
Ok(())
}
fn unmap_page(&mut self, _process: ProcessId, _va: VirtualAddress) {}
fn zero_frame(&mut self, _pa: PhysicalAddress) {}
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
kani::assume(false);
loop {}
}
fn set_timer(&mut self, _duration: TimerDuration) {}
fn current_ticks(&self) -> Ticks {
Ticks::from_raw(0)
}
fn mask_irq(&mut self, _irq: IrqId) {}
fn unmask_irq(&mut self, _irq: IrqId) {}
fn send_ipi(&mut self, _target: CoreId) {}
fn core_id(&self) -> CoreId {
CoreId::from_raw(0)
}
fn num_cores(&self) -> u8 {
1
}
fn halt(&self) -> ! {
// Halting ends the run; there is no state after it to check.
kani::assume(false);
loop {}
}
fn debug_putchar(&self, _byte: u8) {}
}

View File

@@ -1,7 +1,6 @@
use domain::capability::{Capability, CapabilityTable};
use domain::endpoint::Endpoint;
use domain::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap}; use super::{arbitrary_endpoint_cap, insert_cap};
use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource;
use types::generation::Generation; use types::generation::Generation;
use types::id::EndpointId; use types::id::EndpointId;
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
@@ -62,77 +61,3 @@ fn grant_cap_preserves_resource_identity() {
_ => panic!("resource type changed"), _ => panic!("resource type changed"),
} }
} }
#[kani::proof]
fn generation_revocation_invalidates_all_caps() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
let original_gen = *endpoint.generation();
let cap = Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
PermissionSet::from_bits(bits),
original_gen,
);
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, cap);
assert!(table.get(slot).unwrap().generation().is_valid_against(&original_gen));
endpoint.revoke();
let new_gen = *endpoint.generation();
assert!(!table.get(slot).unwrap().generation().is_valid_against(&new_gen));
}
#[kani::proof]
fn cap_created_after_revoke_is_valid() {
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let bits: u8 = kani::any();
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
endpoint.revoke();
let current_gen = *endpoint.generation();
let cap = Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
PermissionSet::from_bits(bits),
current_gen,
);
assert!(cap.generation().is_valid_against(&current_gen));
}
#[kani::proof]
fn multiple_revocations_invalidate_all_prior_caps() {
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
let gen0 = *endpoint.generation();
let cap0 = Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
PermissionSet::empty().with(Permission::Read),
gen0,
);
endpoint.revoke();
let gen1 = *endpoint.generation();
let cap1 = Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
PermissionSet::empty().with(Permission::Read),
gen1,
);
endpoint.revoke();
let gen2 = *endpoint.generation();
assert!(!cap0.generation().is_valid_against(&gen2));
assert!(!cap1.generation().is_valid_against(&gen2));
}

119
crates/kernel/src/smp.rs Normal file
View File

@@ -0,0 +1,119 @@
//! Cores. Every thread has a home core, chosen when it is created, and
//! only ever runs there: it waits in that core's run queue and is never
//! migrated. Making a thread ready from another core sends that core an
//! IPI when it has to look at its queue again (it is idle, or the thread
//! outranks what it is running), and so does taking away the thread a
//! core is running.
use domain::{
scheduler::{Scheduler, SchedulerDecision},
thread::ThreadState,
};
use platform::Platform;
use types::{
bounded::Priority,
id::{CoreId, ThreadId},
};
use crate::Kernel;
impl<P: Platform> Kernel<P> {
/// The core `tid` runs on.
pub fn home(&self, tid: ThreadId) -> CoreId {
self.homes
.get(tid.raw() as usize)
.copied()
.unwrap_or(CoreId::from_raw(0))
}
/// Homes a new thread on `core`.
pub(crate) fn set_home(&mut self, tid: ThreadId, core: CoreId) {
if let Some(h) = self.homes.get_mut(tid.raw() as usize) {
*h = core;
}
}
/// Homes a new thread on the core with the fewest live threads.
pub(crate) fn place(&mut self, tid: ThreadId) {
let cores = self.scheduler.num_cores().max(1);
let load = |core: CoreId| {
self.threads
.iter()
.filter(|(t, _)| *t != tid && self.home(*t) == core)
.count()
};
let best = (0..cores)
.map(CoreId::from_raw)
.min_by_key(|c| load(*c))
.unwrap_or(CoreId::from_raw(0));
self.set_home(tid, best);
}
/// Queues a ready thread on its home core, and kicks that core if it
/// is another one that should look at its queue now.
pub(crate) fn make_ready(&mut self, tid: ThreadId, priority: Priority) {
let home = self.home(tid);
self.scheduler.add(tid, priority, home);
if home == self.platform.core_id() {
return;
}
let outranks = match self.scheduler.current(home) {
None => true,
Some(running) => self
.threads
.get(running)
.is_ok_and(|t| priority > t.priority()),
};
if outranks {
self.platform.send_ipi(home);
}
}
/// Before `tid` is destroyed: if another core is running it, that core
/// must stop and pick something else.
pub(crate) fn evict(&mut self, tid: ThreadId) {
if let Ok(ThreadState::Running(core)) = self.threads.get(tid).map(|t| *t.state())
&& core != self.platform.core_id()
{
self.platform.send_ipi(core);
}
}
/// What `core` should do after an IPI: keep running its thread, or
/// switch to a higher-priority one now waiting. When it has no thread
/// (it was idle, or its thread was destroyed) the caller picks the
/// next one itself.
pub fn reschedule(&mut self, core: CoreId) -> SchedulerDecision {
let Some(current) = self.scheduler.current(core) else {
return SchedulerDecision::Continue;
};
let Ok(thread) = self.threads.get(current) else {
return SchedulerDecision::Continue;
};
let priority = thread.priority();
if self
.scheduler
.highest_waiting(core)
.is_none_or(|p| p <= priority)
{
return SchedulerDecision::Continue;
}
let Some(next) = self.scheduler.next(core) else {
return SchedulerDecision::Continue;
};
if self
.threads
.get_mut(next)
.and_then(|t| t.set_running(core))
.is_err()
{
return SchedulerDecision::Continue;
}
if let Ok(t) = self.threads.get_mut(current)
&& t.preempt().is_ok()
{
self.scheduler.add(current, priority, core);
}
SchedulerDecision::Preempt(next)
}
}

View File

@@ -11,17 +11,18 @@ impl<P: Platform> Kernel<P> {
to_endpoint: types::bounded::CapSlot, to_endpoint: types::bounded::CapSlot,
restrict: types::permission::PermissionSet, restrict: types::permission::PermissionSet,
) -> SyscallOutcome { ) -> SyscallOutcome {
let (resource, permissions) = match self.check_cap( // A copy of the capability itself, so its badge and generation
caller_pid, // travel with it; only the permissions narrow.
slot, let copy = match self.check_cap(caller_pid, slot, types::permission::Permission::Delegate) {
types::permission::Permission::Delegate, Ok(cap) => domain::capability::Capability::new(
) { *cap.resource(),
Ok(cap) => (cap.resource().clone(), *cap.permissions()), cap.permissions().restrict_to(&restrict),
*cap.generation(),
)
.with_badge(cap.badge()),
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
let restricted = permissions.restrict_to(&restrict);
let target_pid = match self.check_cap( let target_pid = match self.check_cap(
caller_pid, caller_pid,
to_endpoint, to_endpoint,
@@ -29,31 +30,24 @@ impl<P: Platform> Kernel<P> {
) { ) {
Ok(cap) => match cap.resource() { Ok(cap) => match cap.resource() {
domain::resource::Resource::Process(pid) => *pid, domain::resource::Resource::Process(pid) => *pid,
_ => return SyscallOutcome::Complete(SyscallResult::Error( _ => {
domain::error::KernelError::PermissionDenied { return SyscallOutcome::Complete(SyscallResult::Error(
slot: to_endpoint, domain::error::KernelError::PermissionDenied {
needed: types::permission::Permission::Execute, slot: to_endpoint,
}, needed: types::permission::Permission::Execute,
)), },
));
}
}, },
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
if let Err(e) = self.grant_cap(target_pid, resource.clone(), restricted) { let parent = self.cap_ref(caller_pid, slot);
let inserted = self.insert_derived(target_pid, copy, parent);
if let Err(e) = inserted {
return SyscallOutcome::Complete(SyscallResult::Error(e)); return SyscallOutcome::Complete(SyscallResult::Error(e));
} }
if let domain::resource::Resource::Memory(region_id) = &resource {
if let Some(region) = self.regions.get(*region_id) {
self.platform.map_shared(
target_pid,
region.base(),
region.size(),
restricted,
);
}
}
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
@@ -62,16 +56,20 @@ impl<P: Platform> Kernel<P> {
caller_pid: types::id::ProcessId, caller_pid: types::id::ProcessId,
slot: types::bounded::CapSlot, slot: types::bounded::CapSlot,
) -> SyscallOutcome { ) -> SyscallOutcome {
let resource = match self.check_cap( SyscallOutcome::Complete(match self.revoke_subtree(caller_pid, slot) {
caller_pid, Ok(()) => SyscallResult::Ok,
slot, Err(e) => SyscallResult::Error(e),
types::permission::Permission::Revoke, })
) { }
Ok(cap) => cap.resource().clone(),
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
self.revoke_resource(&resource); pub(crate) fn handle_drop_cap(
SyscallOutcome::Complete(SyscallResult::Ok) &mut self,
caller_pid: types::id::ProcessId,
slot: types::bounded::CapSlot,
) -> SyscallOutcome {
SyscallOutcome::Complete(match self.delete_cap(caller_pid, slot) {
Ok(()) => SyscallResult::Ok,
Err(e) => SyscallResult::Error(e),
})
} }
} }

View File

@@ -1,50 +1,42 @@
use domain::{fault::Fault, message::Message};
use platform::Platform; use platform::Platform;
use types::{id::ThreadId, permission::Permission};
use super::{Delivery, DeliveryKind, SyscallResult};
use crate::Kernel; use crate::Kernel;
impl<P: Platform> Kernel<P> { impl<P: Platform> Kernel<P> {
pub fn handle_fault( /// A thread faulted. Report it to the process's fault endpoint (if one
&mut self, /// is set, valid and has a receiver waiting), then destroy the process.
tid: types::id::ThreadId, pub fn handle_fault(&mut self, tid: ThreadId, fault: Fault, fault_pc: usize) {
fault: domain::fault::Fault, let Some(pid) = self.thread_process(tid) else {
fault_pc: usize, return;
) {
let pid = match self.threads.get(tid) {
Ok(t) => t.process(),
Err(_) => return,
}; };
let handler = self
// Try to deliver fault to a fault handler endpoint (if configured) .processes
if let Ok(process) = self.processes.get(pid) { .get(pid)
if let Some(handler_slot) = process.fault_handler() { .ok()
if let Ok(cap) = process.capabilities().get(handler_slot) { .and_then(|p| p.fault_handler())
if let domain::resource::Resource::Endpoint(ep_id) = cap.resource() { .and_then(|slot| self.check_cap(pid, slot, Permission::Execute).ok())
let msg = domain::message::Message::from_words([ .and_then(|cap| match cap.resource() {
pid.raw() as usize, domain::resource::Resource::Endpoint(ep) => Some((*ep, cap.badge())),
fault.code(), _ => None,
fault.address(), });
fault_pc, if let Some((ep, badge)) = handler
]); && let Some(receiver) = self
.endpoints
if let Some(ep) = self.endpoints.get_mut(*ep_id) { .get_mut(ep)
if ep.has_waiting_receiver() { .and_then(|e| e.dequeue_receiver())
if let Some(receiver_tid) = ep.dequeue_receiver() { {
self.pending_results[receiver_tid.raw() as usize] = let report =
Some(crate::SyscallResult::Message(msg)); Message::from_words([pid.raw() as usize, fault.code(), fault.address(), fault_pc]);
let delivery = Delivery {
if let Ok(receiver) = self.threads.get_mut(receiver_tid) { kind: DeliveryKind::Message,
let priority = receiver.priority(); badge: badge.raw(),
let _ = receiver.unblock(); words: *report.words(),
let core = self.platform.core_id(); cap: None,
use domain::scheduler::Scheduler; };
self.scheduler.add(receiver_tid, priority, core); self.wake(receiver, SyscallResult::Received(delivery));
}
}
}
}
}
}
}
} }
self.destroy_process(pid); self.destroy_process(pid);

View File

@@ -1,336 +1,569 @@
use platform::Platform; //! Endpoint IPC, notifications and interrupt delivery. docs/ipc.md is the
//! specification; the comments below only point at the non-obvious rules.
use super::{SyscallOutcome, SyscallResult}; use domain::{
capability::Capability,
error::KernelError,
message::Message,
notification::SignalEffect,
resource::Resource,
scheduler::Scheduler,
thread::{BlockReason, ThreadState},
};
use platform::Platform;
use types::{
badge::Badge,
bounded::CapSlot,
generation::Generation,
id::{EndpointId, IrqId, NotificationId, ProcessId, ThreadId},
permission::{Permission, PermissionSet},
};
use super::{Delivery, DeliveryKind, SyscallOutcome, SyscallResult};
use crate::Kernel; use crate::Kernel;
/// A thread named by id and generation, so a stale reference can be told
/// apart from a new thread that reused the id.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThreadRef {
pub tid: ThreadId,
pub generation: Generation,
}
/// A message a blocked sender is waiting to hand over.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Outgoing {
message: Message,
badge: Badge,
/// The sender's endpoint cap had `Delegate`, so a capability may travel.
grant: bool,
}
/// Per-thread IPC bookkeeping.
#[derive(Debug, Clone, Copy, Default)]
pub struct IpcState {
pub(crate) outgoing: Option<Outgoing>,
/// The caller this thread owes a reply to.
pub(crate) reply_to: Option<ThreadRef>,
pub(crate) bound: Option<NotificationId>,
}
fn rw_delegate() -> PermissionSet {
PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)
.with(Permission::Delegate)
}
impl<P: Platform> Kernel<P> { impl<P: Platform> Kernel<P> {
pub(crate) fn handle_create_endpoint( pub(crate) fn ipc(&self, tid: ThreadId) -> Option<&IpcState> {
self.ipc.get(tid.raw() as usize)
}
pub(crate) fn ipc_mut(&mut self, tid: ThreadId) -> Option<&mut IpcState> {
self.ipc.get_mut(tid.raw() as usize)
}
pub(crate) fn set_pending_result(&mut self, tid: ThreadId, result: Option<SyscallResult>) {
if let Some(r) = self.pending_results.get_mut(tid.raw() as usize) {
*r = result;
}
}
fn thread_ref(&self, tid: ThreadId) -> Option<ThreadRef> {
Some(ThreadRef {
tid,
generation: self.threads.generation(tid)?,
})
}
fn is_live(&self, r: ThreadRef) -> bool {
self.threads.generation(r.tid) == Some(r.generation)
}
fn is_blocked_as(&self, tid: ThreadId, f: impl Fn(&BlockReason) -> bool) -> bool {
matches!(self.threads.get(tid).map(|t| *t.state()), Ok(ThreadState::Blocked(r)) if f(&r))
}
/// Blocks the running caller.
pub(crate) fn block_caller(&mut self, tid: ThreadId, reason: BlockReason) -> SyscallOutcome {
if let Ok(t) = self.threads.get_mut(tid) {
let _ = t.block(reason);
}
self.scheduler.remove(tid);
SyscallOutcome::BlockCaller(reason)
}
/// Makes a blocked thread runnable with `result` as its syscall result.
pub(crate) fn wake(&mut self, tid: ThreadId, result: SyscallResult) {
let Ok(thread) = self.threads.get_mut(tid) else {
return;
};
let priority = thread.priority();
if thread.unblock().is_ok() {
self.set_pending_result(tid, Some(result));
self.make_ready(tid, priority);
}
}
/// Releases a caller waiting on `reply` with `NoReply`, if it is still
/// that caller and still waiting.
pub(crate) fn release_caller(&mut self, reply: ThreadRef) {
if self.is_live(reply)
&& self.is_blocked_as(reply.tid, |r| *r == BlockReason::AwaitingReply)
{
self.wake(reply.tid, SyscallResult::Error(KernelError::NoReply));
}
}
fn endpoint_cap(
&self,
pid: ProcessId,
slot: CapSlot,
needed: Permission,
) -> Result<(EndpointId, Badge, bool), KernelError> {
let cap = self.check_cap(pid, slot, needed)?;
match cap.resource() {
Resource::Endpoint(id) => Ok((
*id,
cap.badge(),
cap.permissions().allows(Permission::Delegate),
)),
_ => Err(KernelError::WrongObject),
}
}
fn notification_cap(
&self,
pid: ProcessId,
slot: CapSlot,
needed: Permission,
) -> Result<(NotificationId, Badge, Generation), KernelError> {
let cap = self.check_cap(pid, slot, needed)?;
match cap.resource() {
Resource::Notification(id) => Ok((*id, cap.badge(), *cap.generation())),
_ => Err(KernelError::WrongObject),
}
}
/// Copies the capability in `slot` of `from` into `to`'s table, if the
/// transfer is allowed and still valid. Returns where it landed.
fn transfer_cap(
&mut self, &mut self,
caller_pid: types::id::ProcessId, from: ProcessId,
slot: Option<CapSlot>,
grant: bool,
to: ProcessId,
) -> Option<CapSlot> {
let slot = slot?;
if !grant {
return None;
}
let copy: Capability = self
.check_cap(from, slot, Permission::Delegate)
.ok()?
.clone();
let parent = self.cap_ref(from, slot);
self.insert_derived(to, copy, parent).ok()
}
/// Hands `out` from `sender` to `receiver`. For a call, the receiver's
/// reply slot now names the sender.
fn deliver(
&mut self,
sender: ThreadId,
out: Outgoing,
receiver: ThreadId,
kind: DeliveryKind,
) -> Delivery {
let from = self.thread_process(sender);
let to = self.thread_process(receiver);
let cap = match (from, to) {
(Some(from), Some(to)) => self.transfer_cap(from, out.message.cap(), out.grant, to),
_ => None,
};
if kind == DeliveryKind::Call {
let caller = self.thread_ref(sender);
if let Some(s) = self.ipc_mut(receiver) {
s.reply_to = caller;
}
}
Delivery {
kind,
badge: out.badge.raw(),
words: *out.message.words(),
cap,
}
}
// ---- endpoints ----
pub(crate) fn send(
&mut self,
tid: ThreadId,
pid: ProcessId,
slot: CapSlot,
message: Message,
is_call: bool,
) -> SyscallOutcome { ) -> SyscallOutcome {
let ep_id = match self.create_endpoint() { let (ep, badge, grant) = match self.endpoint_cap(pid, slot, Permission::Execute) {
Ok(v) => v,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let out = Outgoing {
message,
badge,
grant,
};
let kind = if is_call {
DeliveryKind::Call
} else {
DeliveryKind::Message
};
let receiver = self
.endpoints
.get_mut(ep)
.and_then(|e| e.dequeue_receiver());
if let Some(receiver) = receiver {
let delivery = self.deliver(tid, out, receiver, kind);
self.wake(receiver, SyscallResult::Received(delivery));
if !is_call {
return SyscallOutcome::Complete(SyscallResult::Ok);
}
self.block_caller(tid, BlockReason::AwaitingReply);
// Run the server straight away instead of waiting for its turn.
// Only works if it lives on this core (run_now finds it in this
// core's queue only); otherwise make_ready kicked its core.
let core = self.platform.core_id();
if self.scheduler.run_now(core, receiver)
&& self
.threads
.get_mut(receiver)
.and_then(|t| t.set_running(core))
.is_ok()
{
return SyscallOutcome::SwitchTo(receiver);
}
return SyscallOutcome::BlockCaller(BlockReason::AwaitingReply);
}
let queued = self
.endpoints
.get_mut(ep)
.is_some_and(|e| e.enqueue_sender(tid).is_some());
if !queued {
return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull));
}
if let Some(s) = self.ipc_mut(tid) {
s.outgoing = Some(out);
}
let reason = if is_call {
BlockReason::Calling(ep)
} else {
BlockReason::Sending(ep)
};
self.block_caller(tid, reason)
}
/// `drop_reply`: a plain `Receive` abandons any pending reply.
pub(crate) fn receive(
&mut self,
tid: ThreadId,
pid: ProcessId,
slot: CapSlot,
drop_reply: bool,
) -> SyscallOutcome {
let (ep, _, _) = match self.endpoint_cap(pid, slot, Permission::Read) {
Ok(v) => v,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
if drop_reply && let Some(pending) = self.ipc_mut(tid).and_then(|s| s.reply_to.take()) {
self.release_caller(pending);
}
if let Some(n) = self.ipc(tid).and_then(|s| s.bound)
&& let Some(ntfn) = self.notifications.get_mut(n)
&& ntfn.has_bits()
{
let bits = ntfn.take_bits();
return SyscallOutcome::Complete(SyscallResult::Received(Delivery::signal(bits)));
}
let sender = self.endpoints.get_mut(ep).and_then(|e| e.dequeue_sender());
if let Some(sender) = sender {
let is_call = self.is_blocked_as(sender, |r| matches!(r, BlockReason::Calling(_)));
let Some(out) = self.ipc_mut(sender).and_then(|s| s.outgoing.take()) else {
// A queued sender always has a message; treat a missing one
// as a sender that vanished.
return SyscallOutcome::Complete(SyscallResult::Error(KernelError::WouldBlock));
};
let kind = if is_call {
DeliveryKind::Call
} else {
DeliveryKind::Message
};
let delivery = self.deliver(sender, out, tid, kind);
if is_call {
if let Ok(t) = self.threads.get_mut(sender) {
let _ = t.rewait(BlockReason::AwaitingReply);
}
} else {
self.wake(sender, SyscallResult::Ok);
}
return SyscallOutcome::Complete(SyscallResult::Received(delivery));
}
let queued = self
.endpoints
.get_mut(ep)
.is_some_and(|e| e.enqueue_receiver(tid).is_some());
if !queued {
return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull));
}
self.block_caller(tid, BlockReason::Receiving(ep))
}
pub(crate) fn reply(
&mut self,
tid: ThreadId,
pid: ProcessId,
message: Message,
) -> Result<(), KernelError> {
let caller = self
.ipc_mut(tid)
.and_then(|s| s.reply_to.take())
.ok_or(KernelError::NoReply)?;
let waiting = self.is_live(caller)
&& self.is_blocked_as(caller.tid, |r| *r == BlockReason::AwaitingReply);
if !waiting {
// The caller died; the reply has nowhere to go.
return Ok(());
}
let to = self.thread_process(caller.tid);
// A reply may always carry a capability the server can delegate.
let cap = to.and_then(|to| self.transfer_cap(pid, message.cap(), true, to));
let delivery = Delivery {
kind: DeliveryKind::Message,
badge: 0,
words: *message.words(),
cap,
};
self.wake(caller.tid, SyscallResult::Received(delivery));
Ok(())
}
pub(crate) fn reply_recv(
&mut self,
tid: ThreadId,
pid: ProcessId,
slot: CapSlot,
message: Message,
) -> SyscallOutcome {
if let Err(e) = self.endpoint_cap(pid, slot, Permission::Read) {
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
if self.ipc(tid).is_some_and(|s| s.reply_to.is_some()) {
let _ = self.reply(tid, pid, message);
}
self.receive(tid, pid, slot, false)
}
pub(crate) fn mint(
&mut self,
pid: ProcessId,
slot: CapSlot,
badge: Badge,
permissions: PermissionSet,
) -> Result<CapSlot, KernelError> {
let source = self.check_cap(pid, slot, Permission::Delegate)?;
let badgeable = matches!(
source.resource(),
Resource::Endpoint(_) | Resource::Notification(_)
);
// Any capability can be copied with narrower rights; only those a
// receiver can observe the badge of take one, and only once.
if (!badgeable && !badge.is_none()) || !source.badge().is_none() {
return Err(KernelError::InvalidArgument);
}
let minted = Capability::new(
*source.resource(),
source.permissions().restrict_to(&permissions),
*source.generation(),
)
.with_badge(badge);
let parent = self.cap_ref(pid, slot);
self.insert_derived(pid, minted, parent)
}
pub(crate) fn handle_create_endpoint(&mut self, pid: ProcessId) -> SyscallOutcome {
let id = match self.create_endpoint(pid) {
Ok(id) => id, Ok(id) => id,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
let perms = PermissionSet::empty()
let all_perms = types::permission::PermissionSet::empty() .with(Permission::Read)
.with(types::permission::Permission::Read) .with(Permission::Execute)
.with(types::permission::Permission::Write) .with(Permission::Delegate);
.with(types::permission::Permission::Execute) match self.grant_cap(pid, Resource::Endpoint(id), perms) {
.with(types::permission::Permission::Delegate);
match self.grant_cap(
caller_pid,
domain::resource::Resource::Endpoint(ep_id),
all_perms,
) {
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
Err(e) => { Err(e) => {
let _ = self.endpoints.destroy(ep_id); self.endpoints.destroy(id);
SyscallOutcome::Complete(SyscallResult::Error(e)) SyscallOutcome::Complete(SyscallResult::Error(e))
} }
} }
} }
pub(crate) fn handle_send( // ---- notifications ----
&mut self,
caller_tid: types::id::ThreadId,
caller_pid: types::id::ProcessId,
endpoint_slot: types::bounded::CapSlot,
message: domain::message::Message,
) -> SyscallOutcome {
use domain::scheduler::Scheduler;
let ep_id = match self.check_cap( pub fn create_notification(&mut self, owner: ProcessId) -> Result<NotificationId, KernelError> {
caller_pid, self.processes.get(owner)?;
endpoint_slot, self.notifications
types::permission::Permission::Execute, .create(owner)
) { .ok_or(KernelError::NoFreeSlot)
Ok(cap) => match cap.resource() { }
domain::resource::Resource::Endpoint(id) => *id,
_ => return SyscallOutcome::Complete(SyscallResult::Error( pub(crate) fn create_notification_cap(
domain::error::KernelError::InvalidEndpoint(types::id::EndpointId::from_raw(0)), &mut self,
)), pid: ProcessId,
}, ) -> Result<CapSlot, KernelError> {
let id = self.create_notification(pid)?;
self.grant_cap(pid, Resource::Notification(id), rw_delegate())
.inspect_err(|_| {
self.notifications.destroy(id);
})
}
/// Signals `id` with `badge`; shared by `Signal` and interrupts.
fn signal_notification(&mut self, id: NotificationId, badge: Badge) {
let bound_receiving = self
.notifications
.get(id)
.and_then(|n| n.bound())
.is_some_and(|t| self.is_blocked_as(t, |r| matches!(r, BlockReason::Receiving(_))));
let Some(ntfn) = self.notifications.get_mut(id) else {
return;
};
match ntfn.signal(badge, bound_receiving) {
SignalEffect::WakeWaiter(t, bits) => {
self.wake(t, SyscallResult::Received(Delivery::signal(bits)));
}
SignalEffect::WakeBound(t, bits) => {
self.endpoints.remove_thread_from_all(t);
self.wake(t, SyscallResult::Received(Delivery::signal(bits)));
}
SignalEffect::Stored => {}
}
}
pub(crate) fn signal(&mut self, pid: ProcessId, slot: CapSlot) -> Result<(), KernelError> {
let (id, badge, _) = self.notification_cap(pid, slot, Permission::Write)?;
self.signal_notification(id, badge);
Ok(())
}
pub(crate) fn wait(&mut self, tid: ThreadId, pid: ProcessId, slot: CapSlot) -> SyscallOutcome {
let id = match self.notification_cap(pid, slot, Permission::Read) {
Ok((id, _, _)) => id,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
let Some(ntfn) = self.notifications.get_mut(id) else {
let endpoint = match self.endpoints.get_mut(ep_id) { return SyscallOutcome::Complete(SyscallResult::Error(KernelError::NoSuchObject));
Some(ep) => ep,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidEndpoint(ep_id),
)),
}; };
if ntfn.has_bits() {
let bits = ntfn.take_bits();
return SyscallOutcome::Complete(SyscallResult::Received(Delivery::signal(bits)));
}
if !ntfn.add_waiter(tid) {
return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull));
}
self.block_caller(tid, BlockReason::WaitingNotification(id))
}
if endpoint.has_waiting_receiver() { pub(crate) fn poll(&mut self, pid: ProcessId, slot: CapSlot) -> Result<usize, KernelError> {
let receiver_tid = match endpoint.dequeue_receiver() { let (id, _, _) = self.notification_cap(pid, slot, Permission::Read)?;
Some(t) => t, self.notifications
None => return SyscallOutcome::Complete(SyscallResult::Error( .get_mut(id)
domain::error::KernelError::WouldBlock, .map(|n| n.take_bits())
)), .ok_or(KernelError::NoSuchObject)
}; }
self.pending_results[receiver_tid.raw() as usize] = pub(crate) fn bind_notification(
Some(SyscallResult::Message(message)); &mut self,
tid: ThreadId,
pid: ProcessId,
slot: CapSlot,
) -> Result<(), KernelError> {
let (id, _, _) = self.notification_cap(pid, slot, Permission::Read)?;
if self.ipc(tid).is_none_or(|s| s.bound.is_some()) {
return Err(KernelError::InvalidArgument);
}
let ntfn = self
.notifications
.get_mut(id)
.ok_or(KernelError::NoSuchObject)?;
if !ntfn.bind(tid) {
return Err(KernelError::InvalidArgument);
}
if let Some(s) = self.ipc_mut(tid) {
s.bound = Some(id);
}
Ok(())
}
let core = self.platform.core_id(); // ---- interrupts ----
let receiver = match self.threads.get_mut(receiver_tid) { fn interrupt_cap(&self, pid: ProcessId, slot: CapSlot) -> Result<IrqId, KernelError> {
Ok(t) => t, match self.check_cap(pid, slot, Permission::Execute)?.resource() {
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Resource::Interrupt(irq) => Ok(*irq),
}; _ => Err(KernelError::WrongObject),
let _ = receiver.unblock(); }
let _ = receiver.set_running(core); }
let caller = match self.threads.get_mut(caller_tid) { pub(crate) fn set_irq_notification(
Ok(t) => t, &mut self,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), pid: ProcessId,
}; irq_slot: CapSlot,
let caller_priority = caller.priority(); ntfn_slot: CapSlot,
let _ = caller.preempt(); ) -> Result<(), KernelError> {
self.scheduler.add(caller_tid, caller_priority, core); let irq = self.interrupt_cap(pid, irq_slot)?;
let (notification, badge, generation) =
self.scheduler.set_current(core, receiver_tid); self.notification_cap(pid, ntfn_slot, Permission::Write)?;
let route = domain::irq::IrqRoute {
SyscallOutcome::SwitchTo(receiver_tid) notification,
generation,
badge,
};
if self.irqs.set(irq, route) {
Ok(())
} else { } else {
self.pending_messages[caller_tid.raw() as usize] = Some(message); Err(KernelError::InvalidIrq(irq))
let endpoint = match self.endpoints.get_mut(ep_id) {
Some(ep) => ep,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidEndpoint(ep_id),
)),
};
if endpoint.enqueue_sender(caller_tid).is_none() {
self.pending_messages[caller_tid.raw() as usize] = None;
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
));
}
let caller = match self.threads.get_mut(caller_tid) {
Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let _ = caller.block(domain::thread::BlockReason::Sending(endpoint_slot));
self.scheduler.remove(caller_tid);
SyscallOutcome::BlockCaller(domain::thread::BlockReason::Sending(endpoint_slot))
} }
} }
pub(crate) fn handle_receive( pub(crate) fn ack_interrupt(
&mut self, &mut self,
caller_tid: types::id::ThreadId, pid: ProcessId,
caller_pid: types::id::ProcessId, slot: CapSlot,
endpoint_slot: types::bounded::CapSlot, ) -> Result<(), KernelError> {
) -> SyscallOutcome { let irq = self.interrupt_cap(pid, slot)?;
use domain::scheduler::Scheduler; self.platform.unmask_irq(irq);
Ok(())
let ep_id = match self.check_cap(
caller_pid,
endpoint_slot,
types::permission::Permission::Read,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Endpoint(id) => *id,
_ => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidEndpoint(types::id::EndpointId::from_raw(0)),
)),
},
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let endpoint = match self.endpoints.get_mut(ep_id) {
Some(ep) => ep,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidEndpoint(ep_id),
)),
};
if endpoint.has_waiting_sender() {
let sender_tid = match endpoint.dequeue_sender() {
Some(t) => t,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
)),
};
let message = self.pending_messages[sender_tid.raw() as usize].take();
debug_assert!(message.is_some(), "sender in wait queue had no pending message");
let message = message.unwrap_or_else(|| domain::message::Message::empty());
self.pending_results[sender_tid.raw() as usize] = Some(SyscallResult::Ok);
let sender = match self.threads.get_mut(sender_tid) {
Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let priority = sender.priority();
let _ = sender.unblock();
let core = self.platform.core_id();
self.scheduler.add(sender_tid, priority, core);
SyscallOutcome::Complete(SyscallResult::Message(message))
} else {
let endpoint = match self.endpoints.get_mut(ep_id) {
Some(ep) => ep,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidEndpoint(ep_id),
)),
};
if endpoint.enqueue_receiver(caller_tid).is_none() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
));
}
let caller = match self.threads.get_mut(caller_tid) {
Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let _ = caller.block(domain::thread::BlockReason::Receiving(endpoint_slot));
self.scheduler.remove(caller_tid);
SyscallOutcome::BlockCaller(domain::thread::BlockReason::Receiving(endpoint_slot))
}
} }
pub(crate) fn handle_wait_for_interrupt( /// An interrupt fired: mask the line and signal its notification. The
&mut self, /// line stays masked until the handler acknowledges it.
caller_tid: types::id::ThreadId, pub fn handle_interrupt(&mut self, irq: IrqId) {
caller_pid: types::id::ProcessId, self.platform.mask_irq(irq);
irq_slot: types::bounded::CapSlot, let Some(route) = self.irqs.route(irq) else {
) -> SyscallOutcome { return;
use domain::scheduler::Scheduler;
let irq_id = match self.check_cap(
caller_pid,
irq_slot,
types::permission::Permission::Read,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Interrupt(id) => *id,
_ => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidIrq(types::id::IrqId::from_raw(0)),
)),
},
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
// A route set through a capability that has since been revoked, or
let _ = self.irqs.register(irq_id, caller_tid); // to a notification whose slot was reused, is dead.
if self.notifications.generation(route.notification) != Some(route.generation) {
let caller = match self.threads.get_mut(caller_tid) { self.irqs.clear(irq);
Ok(t) => t, return;
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let _ = caller.block(domain::thread::BlockReason::WaitingForInterrupt(irq_slot));
self.scheduler.remove(caller_tid);
SyscallOutcome::BlockCaller(domain::thread::BlockReason::WaitingForInterrupt(irq_slot))
}
pub(crate) fn handle_ack_interrupt(
&mut self,
caller_pid: types::id::ProcessId,
irq_slot: types::bounded::CapSlot,
) -> SyscallOutcome {
let irq_id = match self.check_cap(
caller_pid,
irq_slot,
types::permission::Permission::Write,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Interrupt(id) => *id,
_ => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidIrq(types::id::IrqId::from_raw(0)),
)),
},
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
self.platform.unmask_irq(irq_id);
SyscallOutcome::Complete(SyscallResult::Ok)
}
pub(crate) fn handle_wait_any(
&mut self,
caller_tid: types::id::ThreadId,
caller_pid: types::id::ProcessId,
sources: domain::syscall::WaitSet,
) -> SyscallOutcome {
use domain::scheduler::Scheduler;
if sources.is_empty() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
));
} }
self.signal_notification(route.notification, route.badge);
for ep_slot in sources.endpoints() {
if let Some(slot) = ep_slot {
let ep_id = match self.check_cap(
caller_pid,
*slot,
types::permission::Permission::Read,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Endpoint(id) => *id,
_ => continue,
},
Err(_) => continue,
};
if let Some(ep) = self.endpoints.get_mut(ep_id) {
if ep.has_waiting_sender() {
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::MessageReady(*slot),
));
}
}
}
}
for irq_slot in sources.interrupts() {
if let Some(slot) = irq_slot {
if let Ok(cap) = self.check_cap(
caller_pid,
*slot,
types::permission::Permission::Read,
) {
if let domain::resource::Resource::Interrupt(irq_id) = cap.resource() {
if self.irqs.owner(*irq_id).is_some() {
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::InterruptFired(*slot),
));
}
}
}
}
}
for ep_slot in sources.endpoints() {
if let Some(slot) = ep_slot {
if let Ok(cap) = self.check_cap(caller_pid, *slot, types::permission::Permission::Read) {
if let domain::resource::Resource::Endpoint(id) = cap.resource() {
if let Some(ep) = self.endpoints.get_mut(*id) {
let _ = ep.enqueue_receiver(caller_tid);
}
}
}
}
}
self.wait_sets[caller_tid.raw() as usize] = Some(sources);
let caller = match self.threads.get_mut(caller_tid) {
Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let _ = caller.block(domain::thread::BlockReason::WaitingAny);
self.scheduler.remove(caller_tid);
SyscallOutcome::BlockCaller(domain::thread::BlockReason::WaitingAny)
} }
} }

View File

@@ -1,4 +1,3 @@
use domain::process::MAX_THREADS_PER_PROCESS;
use platform::Platform; use platform::Platform;
use super::{SyscallOutcome, SyscallResult}; use super::{SyscallOutcome, SyscallResult};
@@ -11,14 +10,16 @@ impl<P: Platform> Kernel<P> {
entry: types::address::EntryPoint, entry: types::address::EntryPoint,
stack: types::address::StackPointer, stack: types::address::StackPointer,
) -> SyscallOutcome { ) -> SyscallOutcome {
use domain::scheduler::Scheduler;
use domain::thread::SavedContext; use domain::thread::SavedContext;
let priority = types::bounded::Priority::DEFAULT; let priority = types::bounded::Priority::DEFAULT;
let time_slice = types::time::TimeSlice::DEFAULT; let time_slice = types::time::TimeSlice::DEFAULT;
let context = SavedContext::with_entry_and_stack(entry, stack); let context = SavedContext::with_entry_and_stack(entry, stack);
let tid = match self.threads.spawn_with_context(pid, priority, time_slice, None, context) { let tid = match self
.threads
.spawn_with_context(pid, priority, time_slice, None, context)
{
Ok(t) => t, Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
@@ -28,8 +29,8 @@ impl<P: Platform> Kernel<P> {
return SyscallOutcome::Complete(SyscallResult::Error(e)); return SyscallOutcome::Complete(SyscallResult::Error(e));
} }
let core = self.platform.core_id(); self.place(tid);
self.scheduler.add(tid, priority, core); self.make_ready(tid, priority);
SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize)) SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize))
} }
@@ -39,33 +40,42 @@ impl<P: Platform> Kernel<P> {
create_cap: types::bounded::CapSlot, create_cap: types::bounded::CapSlot,
_image: types::address::DataRef, _image: types::address::DataRef,
) -> SyscallOutcome { ) -> SyscallOutcome {
if let Err(e) = self.check_cap(caller_pid, create_cap, types::permission::Permission::Execute) { if let Err(e) = self.check_cap(
caller_pid,
create_cap,
types::permission::Permission::Execute,
) {
return SyscallOutcome::Complete(SyscallResult::Error(e)); return SyscallOutcome::Complete(SyscallResult::Error(e));
} }
let new_pid = match self.processes.create( let new_pid = match self
domain::capability::CapabilityTable::new(), .processes
None, .create(domain::capability::CapabilityTable::new(), None)
) { {
Ok(p) => p, Ok(p) => p,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
self.process_memory[new_pid.raw() as usize] = if let Err(e) = self.create_address_space(new_pid) {
Some(domain::memory::process_memory::ProcessMemory::new(new_pid, domain::memory::process_memory::ProcessMemory::DEFAULT_PAGE_LIMIT)); let _ = self.processes.destroy(new_pid);
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
match self.grant_cap( match self.grant_cap(
caller_pid, caller_pid,
domain::resource::Resource::Process(new_pid), domain::resource::Resource::Process(new_pid),
// The creator has full authority over what it creates,
// including handing that authority on.
types::permission::PermissionSet::empty() types::permission::PermissionSet::empty()
.with(types::permission::Permission::Execute) .with(types::permission::Permission::Execute)
.with(types::permission::Permission::Read), .with(types::permission::Permission::Read)
.with(types::permission::Permission::Delegate),
) { ) {
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
Err(e) => { Err(e) => {
// Rollback: destroy orphaned process // Rollback: destroy orphaned process
self.teardown_memory(new_pid);
let _ = self.processes.destroy(new_pid); let _ = self.processes.destroy(new_pid);
self.process_memory[new_pid.raw() as usize] = None;
SyscallOutcome::Complete(SyscallResult::Error(e)) SyscallOutcome::Complete(SyscallResult::Error(e))
} }
} }
@@ -78,7 +88,6 @@ impl<P: Platform> Kernel<P> {
entry: types::address::EntryPoint, entry: types::address::EntryPoint,
stack: types::address::StackPointer, stack: types::address::StackPointer,
) -> SyscallOutcome { ) -> SyscallOutcome {
use domain::scheduler::Scheduler;
use domain::thread::SavedContext; use domain::thread::SavedContext;
let target_pid = match self.check_cap( let target_pid = match self.check_cap(
@@ -88,12 +97,14 @@ impl<P: Platform> Kernel<P> {
) { ) {
Ok(cap) => match cap.resource() { Ok(cap) => match cap.resource() {
domain::resource::Resource::Process(pid) => *pid, domain::resource::Resource::Process(pid) => *pid,
_ => return SyscallOutcome::Complete(SyscallResult::Error( _ => {
domain::error::KernelError::PermissionDenied { return SyscallOutcome::Complete(SyscallResult::Error(
slot: process_cap, domain::error::KernelError::PermissionDenied {
needed: types::permission::Permission::Execute, slot: process_cap,
}, needed: types::permission::Permission::Execute,
)), },
));
}
}, },
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
@@ -102,18 +113,25 @@ impl<P: Platform> Kernel<P> {
let time_slice = types::time::TimeSlice::DEFAULT; let time_slice = types::time::TimeSlice::DEFAULT;
let context = SavedContext::with_entry_and_stack(entry, stack); let context = SavedContext::with_entry_and_stack(entry, stack);
let tid = match self.threads.spawn_with_context(target_pid, priority, time_slice, None, context) { let tid = match self
.threads
.spawn_with_context(target_pid, priority, time_slice, None, context)
{
Ok(t) => t, Ok(t) => t,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
if let Err(e) = self.processes.get_mut(target_pid).and_then(|p| p.add_thread(tid)) { if let Err(e) = self
.processes
.get_mut(target_pid)
.and_then(|p| p.add_thread(tid))
{
let _ = self.threads.destroy(tid); let _ = self.threads.destroy(tid);
return SyscallOutcome::Complete(SyscallResult::Error(e)); return SyscallOutcome::Complete(SyscallResult::Error(e));
} }
let core = self.platform.core_id(); self.place(tid);
self.scheduler.add(tid, priority, core); self.make_ready(tid, priority);
SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize)) SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize))
} }
@@ -122,20 +140,30 @@ impl<P: Platform> Kernel<P> {
caller_pid: types::id::ProcessId, caller_pid: types::id::ProcessId,
process_cap: types::bounded::CapSlot, process_cap: types::bounded::CapSlot,
) -> SyscallOutcome { ) -> SyscallOutcome {
let target_pid = match self.check_cap(caller_pid, process_cap, types::permission::Permission::Execute) { let target_pid = match self.check_cap(
caller_pid,
process_cap,
types::permission::Permission::Execute,
) {
Ok(cap) => match cap.resource() { Ok(cap) => match cap.resource() {
domain::resource::Resource::Process(pid) => *pid, domain::resource::Resource::Process(pid) => *pid,
_ => return SyscallOutcome::Complete(SyscallResult::Error( _ => {
domain::error::KernelError::PermissionDenied { return SyscallOutcome::Complete(SyscallResult::Error(
slot: process_cap, domain::error::KernelError::PermissionDenied {
needed: types::permission::Permission::Execute, slot: process_cap,
}, needed: types::permission::Permission::Execute,
)), },
));
}
}, },
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
self.destroy_process(target_pid); self.destroy_process(target_pid);
if target_pid == caller_pid {
// The caller no longer exists; nothing to return to.
return SyscallOutcome::TerminateCaller;
}
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
} }

View File

@@ -1,156 +1,135 @@
use platform::Platform; use platform::Platform;
use types::address::{PhysicalAddress, VirtualAddress}; use types::{
address::{PhysicalAddress, VirtualAddress},
bounded::CapSlot,
bounded::PageCount,
id::ProcessId,
mapping::PagePerms,
};
use super::{SyscallOutcome, SyscallResult}; use super::{SyscallOutcome, SyscallResult};
use crate::Kernel; use crate::Kernel;
use domain::{error::KernelError, resource::Resource};
use types::permission::Permission;
const USER_VA_LIMIT: usize = 0x80000000; fn complete(result: Result<SyscallResult, KernelError>) -> SyscallOutcome {
SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error))
}
impl<P: Platform> Kernel<P> { impl<P: Platform> Kernel<P> {
pub(crate) fn handle_request_pages( pub(crate) fn handle_request_pages(
&mut self, &mut self,
pid: types::id::ProcessId, pid: ProcessId,
count: types::bounded::PageCount, count: PageCount,
) -> SyscallOutcome { ) -> SyscallOutcome {
let pages = count.value() as usize; let pages = count.value() as usize;
complete(
match &mut self.process_memory[pid.raw() as usize] { self.find_free_heap_range(pid, pages)
Some(pm) => { .ok_or(KernelError::OutOfMemory)
if pm.grant(pages).is_err() { .and_then(|va| {
return SyscallOutcome::Complete(SyscallResult::Error( self.map_fresh_range(pid, va, pages, PagePerms::ReadWrite)
domain::error::KernelError::OutOfMemory, .map(|()| SyscallResult::Value(va.raw()))
)); }),
} )
}
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidProcess(pid),
)),
}
let phys_addr = match self.page_pool.allocate(pages) {
Some(a) => a,
None => {
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
let _ = pm.release(pages);
}
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
};
let user_va = match &mut self.process_memory[pid.raw() as usize] {
Some(pm) => {
let va = pm.alloc_user_va(pages);
pm.record_allocation(phys_addr, pages);
va
}
None => {
self.page_pool.free(phys_addr, pages);
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidProcess(pid),
));
}
};
for i in 0..pages {
let page_va = VirtualAddress::from_raw(user_va.raw() + i * 4096);
let page_pa = PhysicalAddress::from_raw(phys_addr.raw() + i * 4096);
self.platform.map_user_page(pid, page_va, page_pa, true, false);
}
let region = domain::memory::MemoryRegion::new(
types::id::RegionId::from_raw(0),
pid,
phys_addr,
count,
types::generation::Generation::initial(),
);
let _ = self.regions.insert(region);
SyscallOutcome::Complete(SyscallResult::Value(user_va.raw()))
}
pub(crate) fn handle_map_page(
&mut self,
caller_pid: types::id::ProcessId,
process_cap: types::bounded::CapSlot,
src_va: VirtualAddress,
dst_va: VirtualAddress,
executable: bool,
) -> SyscallOutcome {
// Prevent mapping kernel memory into userspace
if src_va.raw() >= USER_VA_LIMIT {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::PermissionDenied {
slot: process_cap,
needed: types::permission::Permission::Read,
},
));
}
let target_pid = match self.check_cap(
caller_pid,
process_cap,
types::permission::Permission::Execute,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Process(pid) => *pid,
_ => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::PermissionDenied {
slot: process_cap,
needed: types::permission::Permission::Execute,
},
)),
},
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let pa = match self.platform.translate_va(caller_pid, src_va) {
Some(pa) => pa,
None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
)),
};
self.platform.map_user_page(target_pid, dst_va, pa, !executable, executable);
SyscallOutcome::Complete(SyscallResult::Ok)
} }
pub(crate) fn handle_release_pages( pub(crate) fn handle_release_pages(
&mut self, &mut self,
pid: types::id::ProcessId, pid: ProcessId,
addr: VirtualAddress, addr: VirtualAddress,
count: types::bounded::PageCount, count: PageCount,
) -> SyscallOutcome { ) -> SyscallOutcome {
let pages = count.value() as usize; complete(
let phys = match self.platform.translate_va(pid, addr) { self.release_range(pid, addr, count.value() as usize)
Some(pa) => pa, .map(|()| SyscallResult::Ok),
None => return SyscallOutcome::Complete(SyscallResult::Error( )
domain::error::KernelError::OutOfMemory, }
)),
/// Moves one of the caller's pages into the process named by
/// `process_cap`. Executable pages are mapped read-execute, others
/// read-write.
pub(crate) fn handle_map_page(
&mut self,
caller_pid: ProcessId,
process_cap: CapSlot,
src_va: VirtualAddress,
dst_va: VirtualAddress,
executable: bool,
) -> SyscallOutcome {
let target = match self.check_cap(caller_pid, process_cap, Permission::Execute) {
Ok(cap) => match cap.resource() {
Resource::Process(pid) => *pid,
_ => {
return complete(Err(KernelError::PermissionDenied {
slot: process_cap,
needed: Permission::Execute,
}));
}
},
Err(e) => return complete(Err(e)),
}; };
let perms = if executable {
let region_id = match self.regions.find_by_base(pid, phys) { PagePerms::ReadExecute
Some((rid, _)) => rid, } else {
None => return SyscallOutcome::Complete(SyscallResult::Error( PagePerms::ReadWrite
domain::error::KernelError::OutOfMemory,
)),
}; };
complete(
self.move_page(caller_pid, src_va, target, dst_va, perms)
.map(|()| SyscallResult::Ok),
)
}
if !self.page_pool.free(phys, pages) { pub(crate) fn create_region_cap(
return SyscallOutcome::Complete(SyscallResult::Error( &mut self,
domain::error::KernelError::OutOfMemory, pid: ProcessId,
)); pages: PageCount,
) -> Result<CapSlot, KernelError> {
let id = self.create_pool_region(pid, pages)?;
let all = types::permission::PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)
.with(Permission::Execute)
.with(Permission::Delegate);
self.grant_cap(pid, Resource::Memory(id), all)
}
/// Maps the region behind `slot` into the caller. The capability must
/// allow every access `perms` grants; the mapping lasts as long as
/// the capability.
pub(crate) fn map_region_cap(
&mut self,
pid: ProcessId,
slot: CapSlot,
va: VirtualAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
let cap = self.check_cap(pid, slot, Permission::Read)?;
let needed = match perms {
PagePerms::ReadOnly => Permission::Read,
PagePerms::ReadWrite => Permission::Write,
PagePerms::ReadExecute => Permission::Execute,
};
if !cap.permissions().allows(needed) {
return Err(KernelError::PermissionDenied { slot, needed });
} }
let Resource::Memory(id) = *cap.resource() else {
return Err(KernelError::WrongObject);
};
let via = self.cap_ref(pid, slot);
self.map_region_via(id, pid, va, perms, via)
}
let _ = self.regions.remove(region_id); /// The physical base of the region behind `slot`. Needs Read on the
/// capability; pool regions are physically contiguous, so the base is
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] { /// enough to program a device.
let _ = pm.release(pages); pub(crate) fn region_address_cap(
} &self,
pid: ProcessId,
SyscallOutcome::Complete(SyscallResult::Ok) slot: CapSlot,
) -> Result<PhysicalAddress, KernelError> {
let Resource::Memory(id) = *self.check_cap(pid, slot, Permission::Read)?.resource() else {
return Err(KernelError::WrongObject);
};
self.region_base(id).ok_or(KernelError::InvalidRegion)
} }
} }

View File

@@ -5,28 +5,66 @@ mod lifecycle;
mod memory; mod memory;
mod simple; mod simple;
use domain::{error::KernelError, message::Message, syscall::Syscall}; use domain::{error::KernelError, syscall::Syscall};
use platform::Platform; use platform::Platform;
use types::{bounded::CapSlot, id::ThreadId}; use types::{bounded::CapSlot, id::ThreadId, syscall_abi::ErrorCode};
use crate::Kernel; use crate::Kernel;
pub use ipc::{IpcState, ThreadRef};
pub use types::syscall_abi::{Delivery, DeliveryKind};
pub enum SyscallOutcome { pub enum SyscallOutcome {
Complete(SyscallResult), Complete(SyscallResult),
BlockCaller(domain::thread::BlockReason), BlockCaller(domain::thread::BlockReason),
/// The caller blocked and `ThreadId` should run next.
SwitchTo(ThreadId), SwitchTo(ThreadId),
TerminateCaller, TerminateCaller,
} }
#[derive(Debug)]
pub enum SyscallResult { pub enum SyscallResult {
Ok, Ok,
Value(usize), Value(usize),
Slot(CapSlot), Slot(CapSlot),
Message(Message), Received(Delivery),
WakeReason(domain::syscall::WakeReason),
Error(KernelError), Error(KernelError),
} }
/// The status code userspace sees for a kernel error.
pub fn error_code(error: &KernelError) -> ErrorCode {
use KernelError::*;
match error {
InvalidSlot(_) | RevokedCapability(_) | NoSuchObject | InvalidEndpoint(_) => {
ErrorCode::InvalidCap
}
PermissionDenied { .. } | ProcessCreateDenied | WritableAndExecutable => {
ErrorCode::PermissionDenied
}
WrongObject => ErrorCode::WrongObject,
NoFreeSlot => ErrorCode::NoFreeSlot,
OutOfMemory => ErrorCode::OutOfMemory,
InvalidAddress => ErrorCode::InvalidAddress,
AddressInUse => ErrorCode::AddressInUse,
NoReply => ErrorCode::NoReply,
TooManyThreadsInProcess | TooManyProcesses | TooManyThreads | EndpointFull => {
ErrorCode::LimitReached
}
InvalidProcess(_)
| InvalidThread(_)
| InvalidStateTransition(_)
| InvalidIrq(_)
| InvalidRegion
| IrqAlreadyOwned(_)
| WouldBlock
| InvalidArgument => ErrorCode::InvalidArgument,
}
}
fn complete(result: Result<SyscallResult, KernelError>) -> SyscallOutcome {
SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error))
}
impl<P: Platform> Kernel<P> { impl<P: Platform> Kernel<P> {
pub fn handle_syscall(&mut self, syscall: Syscall) -> SyscallOutcome { pub fn handle_syscall(&mut self, syscall: Syscall) -> SyscallOutcome {
let (tid, pid) = match self.caller() { let (tid, pid) = match self.caller() {
@@ -43,26 +81,81 @@ impl<P: Platform> Kernel<P> {
Syscall::RequestPages { count } => self.handle_request_pages(pid, count), Syscall::RequestPages { count } => self.handle_request_pages(pid, count),
Syscall::ReleasePages { addr, count } => self.handle_release_pages(pid, addr, count), Syscall::ReleasePages { addr, count } => self.handle_release_pages(pid, addr, count),
Syscall::SpawnThread { entry, stack } => self.handle_spawn_thread(pid, entry, stack), Syscall::SpawnThread { entry, stack } => self.handle_spawn_thread(pid, entry, stack),
Syscall::CreateProcess { create_cap, image } => self.handle_create_process(pid, create_cap, image), Syscall::CreateProcess { create_cap, image } => {
self.handle_create_process(pid, create_cap, image)
}
Syscall::Kill { process } => self.handle_kill(pid, process), Syscall::Kill { process } => self.handle_kill(pid, process),
Syscall::Revoke { slot } => self.handle_revoke(pid, slot), Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
Syscall::Delegate { slot, to_endpoint, permissions } => { Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),
self.handle_delegate(pid, slot, to_endpoint, permissions) Syscall::CreateRegion { pages } => {
complete(self.create_region_cap(pid, pages).map(SyscallResult::Slot))
} }
Syscall::Send { endpoint, message } => self.handle_send(tid, pid, endpoint, message), Syscall::MapRegion { region, va, perms } => complete(
Syscall::Receive { endpoint } => self.handle_receive(tid, pid, endpoint), self.map_region_cap(pid, region, va, perms)
Syscall::WaitForInterrupt { irq } => self.handle_wait_for_interrupt(tid, pid, irq), .map(|()| SyscallResult::Ok),
Syscall::AcknowledgeInterrupt { irq } => self.handle_ack_interrupt(pid, irq), ),
Syscall::WaitAny { sources } => self.handle_wait_any(tid, pid, sources), Syscall::RegionAddress { region } => complete(
Syscall::Halt => self.handle_halt(pid), self.region_address_cap(pid, region)
.map(|pa| SyscallResult::Value(pa.raw())),
),
Syscall::Delegate {
slot,
to_endpoint,
permissions,
} => self.handle_delegate(pid, slot, to_endpoint, permissions),
Syscall::Mint {
slot,
badge,
permissions,
} => complete(
self.mint(pid, slot, badge, permissions)
.map(SyscallResult::Slot),
),
Syscall::Send { endpoint, message } => self.send(tid, pid, endpoint, message, false),
Syscall::Call { endpoint, message } => self.send(tid, pid, endpoint, message, true),
Syscall::Receive { endpoint } => self.receive(tid, pid, endpoint, true),
Syscall::Reply { message } => {
complete(self.reply(tid, pid, message).map(|()| SyscallResult::Ok))
}
Syscall::ReplyRecv { endpoint, message } => {
self.reply_recv(tid, pid, endpoint, message)
}
Syscall::CreateNotification => {
complete(self.create_notification_cap(pid).map(SyscallResult::Slot))
}
Syscall::Signal { notification } => {
complete(self.signal(pid, notification).map(|()| SyscallResult::Ok))
}
Syscall::Wait { notification } => self.wait(tid, pid, notification),
Syscall::Poll { notification } => complete(
self.poll(pid, notification)
.map(|bits| SyscallResult::Received(Delivery::signal(bits))),
),
Syscall::BindNotification { notification } => complete(
self.bind_notification(tid, pid, notification)
.map(|()| SyscallResult::Ok),
),
Syscall::SetIrqNotification { irq, notification } => complete(
self.set_irq_notification(pid, irq, notification)
.map(|()| SyscallResult::Ok),
),
Syscall::AcknowledgeInterrupt { irq } => {
complete(self.ack_interrupt(pid, irq).map(|()| SyscallResult::Ok))
}
Syscall::Halt { control } => self.handle_halt(pid, control),
Syscall::CreateEndpoint => self.handle_create_endpoint(pid), Syscall::CreateEndpoint => self.handle_create_endpoint(pid),
Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte), Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte),
Syscall::MapPage { process, src_va, dst_va, executable } => { Syscall::MapPage {
self.handle_map_page(pid, process, src_va, dst_va, executable) process,
} src_va,
Syscall::SpawnThreadIn { process, entry, stack } => { dst_va,
self.handle_spawn_thread_in(pid, process, entry, stack) executable,
} } => self.handle_map_page(pid, process, src_va, dst_va, executable),
Syscall::SpawnThreadIn {
process,
entry,
stack,
} => self.handle_spawn_thread_in(pid, process, entry, stack),
} }
} }
} }

View File

@@ -1,4 +1,6 @@
use domain::{error::KernelError, resource::Resource};
use platform::Platform; use platform::Platform;
use types::{bounded::CapSlot, id::ProcessId, permission::Permission};
use super::{SyscallOutcome, SyscallResult}; use super::{SyscallOutcome, SyscallResult};
use crate::Kernel; use crate::Kernel;
@@ -31,15 +33,26 @@ impl<P: Platform> Kernel<P> {
match self.scheduler.next(core) { match self.scheduler.next(core) {
Some(next) if next != caller_tid => { Some(next) if next != caller_tid => {
if self.threads.get_mut(next).and_then(|t| t.set_running(core)).is_ok() { if self
.threads
.get_mut(next)
.and_then(|t| t.set_running(core))
.is_ok()
{
SyscallOutcome::SwitchTo(next) SyscallOutcome::SwitchTo(next)
} else { } else {
let _ = self.threads.get_mut(caller_tid).map(|t| t.set_running(core)); let _ = self
.threads
.get_mut(caller_tid)
.map(|t| t.set_running(core));
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
} }
_ => { _ => {
let _ = self.threads.get_mut(caller_tid).map(|t| t.set_running(core)); let _ = self
.threads
.get_mut(caller_tid)
.map(|t| t.set_running(core));
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
} }
@@ -59,10 +72,7 @@ impl<P: Platform> Kernel<P> {
caller_tid: types::id::ThreadId, caller_tid: types::id::ThreadId,
pid: types::id::ProcessId, pid: types::id::ProcessId,
) -> SyscallOutcome { ) -> SyscallOutcome {
use domain::scheduler::Scheduler; self.cleanup_threads(&[Some(caller_tid)]);
self.scheduler.remove(caller_tid);
let _ = self.threads.destroy(caller_tid);
if let Ok(process) = self.processes.get_mut(pid) { if let Ok(process) = self.processes.get_mut(pid) {
process.remove_thread(caller_tid); process.remove_thread(caller_tid);
@@ -79,15 +89,13 @@ impl<P: Platform> Kernel<P> {
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
pub(crate) fn handle_halt( /// Halts the machine for a holder of a SystemControl capability with
&mut self, /// Execute; anyone else gets an error and nothing happens.
pid: types::id::ProcessId, pub(crate) fn handle_halt(&mut self, pid: ProcessId, control: CapSlot) -> SyscallOutcome {
) -> SyscallOutcome { match self.check_cap(pid, control, Permission::Execute) {
if pid.raw() != 0 { Ok(cap) if *cap.resource() == Resource::SystemControl => self.platform.halt(),
return SyscallOutcome::Complete(SyscallResult::Error( Ok(_) => SyscallOutcome::Complete(SyscallResult::Error(KernelError::WrongObject)),
domain::error::KernelError::ProcessCreateDenied, Err(e) => SyscallOutcome::Complete(SyscallResult::Error(e)),
));
} }
self.platform.halt();
} }
} }

View File

@@ -0,0 +1,100 @@
use crate::abi::{decode, encode, unknown_syscall};
use crate::syscall::SyscallResult;
use domain::syscall::Syscall;
use types::syscall_abi::*;
/// Every syscall number the ABI defines decodes with ordinary arguments,
/// so the decoder cannot silently drop one.
#[test]
fn every_defined_syscall_decodes() {
let numbers = [
SYS_SEND,
SYS_RECEIVE,
SYS_DELEGATE,
SYS_ACKNOWLEDGE_INTERRUPT,
SYS_CREATE_PROCESS,
SYS_KILL,
SYS_REVOKE,
SYS_SPAWN_THREAD,
SYS_EXIT_THREAD,
SYS_REQUEST_PAGES,
SYS_RELEASE_PAGES,
SYS_GET_TIME,
SYS_GET_PROCESS_ID,
SYS_YIELD,
SYS_EXIT,
SYS_HALT,
SYS_CREATE_ENDPOINT,
SYS_DEBUG_PUTCHAR,
SYS_MAP_PAGE,
SYS_SPAWN_THREAD_IN,
SYS_DROP_CAP,
SYS_CALL,
SYS_REPLY,
SYS_REPLY_RECV,
SYS_MINT,
SYS_CREATE_NOTIFICATION,
SYS_SIGNAL,
SYS_WAIT,
SYS_POLL,
SYS_BIND_NOTIFICATION,
SYS_SET_IRQ_NOTIFICATION,
SYS_CREATE_REGION,
SYS_MAP_REGION,
SYS_REGION_ADDRESS,
];
// slot 1, slot 2, small values, no cap
let args = [1, 2, 1, 0, 0, NONE];
for nr in numbers {
assert!(decode(nr, &args).is_some(), "syscall {nr} does not decode");
}
}
#[test]
fn retired_and_unknown_numbers_do_not_decode() {
for nr in [3, 5, 36, 1000, usize::MAX] {
assert!(decode(nr, &[0; 6]).is_none(), "{nr} decoded");
}
}
#[test]
fn out_of_range_arguments_do_not_decode() {
assert!(decode(SYS_RECEIVE, &[64, 0, 0, 0, 0, 0]).is_none());
assert!(
decode(SYS_SEND, &[1, 0, 0, 0, 0, 64]).is_none(),
"bad cap slot"
);
assert!(
decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(),
"zero pages"
);
assert!(decode(SYS_REQUEST_PAGES, &[1025, 0, 0, 0, 0, 0]).is_none());
assert!(
decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(),
"bad perms"
);
assert!(
decode(SYS_DELEGATE, &[1, 2, 256, 0, 0, 0]).is_none(),
"perm bits overflow u8"
);
}
#[test]
fn none_means_no_capability() {
match decode(SYS_SEND, &[1, 9, 8, 7, 6, NONE]) {
Some(Syscall::Send { message, .. }) => {
assert_eq!(message.cap(), None);
assert_eq!(message.words(), &[9, 8, 7, 6]);
}
_ => panic!("send did not decode"),
}
}
#[test]
fn results_follow_the_status_convention() {
assert_eq!(encode(&SyscallResult::Ok), [0; 8]);
assert_eq!(encode(&SyscallResult::Value(7))[..2], [0, 7]);
let err = encode(&SyscallResult::Error(domain::error::KernelError::NoReply));
assert_eq!(err[0], ErrorCode::NoReply as usize);
assert_eq!(unknown_syscall()[0], ErrorCode::UnknownSyscall as usize);
}

View File

@@ -0,0 +1,323 @@
use super::*;
use crate::{BoardDevice, SyscallOutcome, SyscallResult};
use domain::{error::KernelError, resource::Resource, syscall::Syscall};
use types::{
address::{PhysicalAddress, VirtualAddress},
badge::Badge,
bounded::{CapSlot, PageCount, Priority},
id::IrqId,
mapping::PagePerms,
permission::{Permission, PermissionSet},
time::TimeSlice,
};
const UART: usize = 0x1000_0000;
const VIRTIO: usize = 0x1000_1000;
fn device(base: usize, pages: u16, irq: Option<u16>) -> BoardDevice {
BoardDevice {
base: PhysicalAddress::from_raw(base),
pages: PageCount::new(pages).unwrap(),
irq: irq.map(IrqId::from_raw),
}
}
fn board() -> [BoardDevice; 2] {
[device(UART, 1, Some(10)), device(VIRTIO, 1, Some(1))]
}
/// Init after boot with the board registered and its authority granted.
fn booted() -> (Kernel<MockPlatform>, ProcessId) {
let (mut k, init) = kernel_with_process();
k.register_board(init, &board()).unwrap();
k.grant_boot_authority(init, &board()).unwrap();
(k, init)
}
fn slot_holding(k: &Kernel<MockPlatform>, pid: ProcessId, resource: Resource) -> CapSlot {
k.processes
.get(pid)
.unwrap()
.capabilities()
.iter()
.find(|(_, c)| *c.resource() == resource)
.map(|(s, _)| s)
.expect("capability present")
}
fn perms_of(k: &Kernel<MockPlatform>, pid: ProcessId, slot: CapSlot) -> PermissionSet {
*k.processes
.get(pid)
.unwrap()
.capabilities()
.get(slot)
.unwrap()
.permissions()
}
fn error_of(outcome: SyscallOutcome) -> KernelError {
match outcome {
SyscallOutcome::Complete(SyscallResult::Error(e)) => e,
_ => panic!("expected an error"),
}
}
// ---- halt ----
#[test]
fn init_cannot_halt_without_a_system_control_cap() {
let (mut k, _) = kernel_with_process();
let e = error_of(k.handle_syscall(Syscall::Halt {
control: CapSlot::new(0).unwrap(),
}));
assert!(
matches!(e, KernelError::WrongObject),
"slot 0 is ProcessCreate"
);
let e = error_of(k.handle_syscall(Syscall::Halt {
control: CapSlot::new(9).unwrap(),
}));
assert!(matches!(e, KernelError::InvalidSlot(_)));
}
#[test]
#[should_panic(expected = "halt")]
fn system_control_cap_halts() {
let (mut k, init) = booted();
let control = slot_holding(&k, init, Resource::SystemControl);
k.handle_syscall(Syscall::Halt { control });
}
#[test]
fn halt_needs_execute_on_the_cap() {
let (mut k, init) = booted();
let control = slot_holding(&k, init, Resource::SystemControl);
let weak = k
.mint(
init,
control,
Badge::new(0),
PermissionSet::empty().with(Permission::Delegate),
)
.unwrap();
let e = error_of(k.handle_syscall(Syscall::Halt { control: weak }));
assert!(matches!(e, KernelError::PermissionDenied { .. }));
}
#[test]
#[should_panic(expected = "halt")]
fn any_holder_of_the_cap_may_halt_not_just_init() {
let (mut k, init) = booted();
let other = k.create_process_internal().unwrap();
let tid = k.spawn_thread_for_test(other, Priority::DEFAULT, TimeSlice::DEFAULT);
k.grant_cap_for_test(
other,
Resource::SystemControl,
PermissionSet::empty().with(Permission::Execute),
);
let _ = init;
k.switch_to_thread_for_test(tid);
let control = slot_holding(&k, other, Resource::SystemControl);
k.handle_syscall(Syscall::Halt { control });
}
// ---- boot authority ----
#[test]
fn init_gets_mmio_irq_and_control_caps_it_can_delegate() {
let (k, init) = booted();
let delegate = Permission::Delegate;
for d in board() {
let region = k.device_region_at(d.base).unwrap();
let mem = slot_holding(&k, init, Resource::Memory(region));
let p = perms_of(&k, init, mem);
assert!(p.allows(Permission::Read) && p.allows(Permission::Write) && p.allows(delegate));
assert!(!p.allows(Permission::Execute), "MMIO is never executable");
let irq = slot_holding(&k, init, Resource::Interrupt(d.irq.unwrap()));
let p = perms_of(&k, init, irq);
assert!(p.allows(Permission::Execute) && p.allows(delegate));
}
let control = slot_holding(&k, init, Resource::SystemControl);
let p = perms_of(&k, init, control);
assert!(p.allows(Permission::Execute) && p.allows(delegate));
}
#[test]
fn boot_authority_keeps_existing_slots() {
let (mut k, init) = kernel_with_process();
let before = k.cap_slots_for_test(init);
k.register_board(init, &board()).unwrap();
k.grant_boot_authority(init, &board()).unwrap();
let after = k.cap_slots_for_test(init);
for (b, a) in before.iter().zip(after.iter()) {
assert!(!b || *a, "an existing slot was disturbed");
}
assert_eq!(after.iter().filter(|s| **s).count(), 1 + 2 + 2 + 1);
}
#[test]
fn init_can_map_a_device_through_its_cap() {
let (mut k, init) = booted();
let region = slot_holding(
&k,
init,
Resource::Memory(k.device_region_at(PhysicalAddress::from_raw(UART)).unwrap()),
);
let outcome = k.handle_syscall(Syscall::MapRegion {
region,
va: VirtualAddress::from_raw(0x10_0000),
perms: PagePerms::ReadWrite,
});
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Ok)
));
let e = error_of(k.handle_syscall(Syscall::MapRegion {
region,
va: VirtualAddress::from_raw(0x20_0000),
perms: PagePerms::ReadExecute,
}));
assert!(matches!(e, KernelError::PermissionDenied { .. }));
}
#[test]
fn init_can_route_a_board_irq() {
let (mut k, init) = booted();
let irq = slot_holding(&k, init, Resource::Interrupt(IrqId::from_raw(1)));
let notification = k.create_notification_cap(init).unwrap();
let outcome = k.handle_syscall(Syscall::SetIrqNotification { irq, notification });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Ok)
));
}
#[test]
fn granting_needs_a_registered_board() {
let (mut k, init) = kernel_with_process();
assert!(matches!(
k.grant_boot_authority(init, &board()),
Err(KernelError::NoSuchObject)
));
}
#[test]
fn granting_needs_the_regions_to_be_inits() {
let (mut k, init) = kernel_with_process();
let other = k.create_process_internal().unwrap();
k.register_board(other, &board()).unwrap();
assert!(matches!(
k.grant_boot_authority(init, &board()),
Err(KernelError::NoSuchObject)
));
}
#[test]
fn board_devices_may_not_overlap() {
let (mut k, init) = kernel_with_process();
let overlapping = [device(UART, 2, None), device(UART + 0x1000, 1, None)];
assert!(matches!(
k.register_board(init, &overlapping),
Err(KernelError::AddressInUse)
));
}
#[test]
fn board_devices_may_not_alias_the_frame_pool() {
let (mut k, init) = kernel_with_process();
assert!(matches!(
k.register_board(init, &[device(0x8400_0000, 1, None)]),
Err(KernelError::InvalidAddress)
));
}
#[test]
fn board_devices_must_be_page_aligned_and_in_range() {
let (mut k, init) = kernel_with_process();
assert!(matches!(
k.register_board(init, &[device(UART + 8, 1, None)]),
Err(KernelError::InvalidAddress)
));
assert!(matches!(
k.register_board(init, &[device(usize::MAX & !0xfff, 2, None)]),
Err(KernelError::InvalidAddress)
));
}
#[test]
fn board_irqs_must_be_routable() {
let (mut k, init) = kernel_with_process();
let bad = IrqId::from_raw(u16::MAX);
assert!(matches!(
k.register_board(init, &[device(UART, 1, Some(u16::MAX))]),
Err(KernelError::InvalidIrq(i)) if i == bad
));
}
// ---- region address ----
#[test]
fn region_address_reports_the_physical_base() {
let (mut k, init) = booted();
let pool = k
.create_region_cap(init, PageCount::new(2).unwrap())
.unwrap();
let outcome = k.handle_syscall(Syscall::RegionAddress { region: pool });
let SyscallOutcome::Complete(SyscallResult::Value(pa)) = outcome else {
panic!("expected an address");
};
let Resource::Memory(id) = *k
.processes
.get(init)
.unwrap()
.capabilities()
.get(pool)
.unwrap()
.resource()
else {
panic!("not a region cap");
};
assert_eq!(Some(pa), k.region_base(id).map(|b| b.raw()));
let uart = slot_holding(
&k,
init,
Resource::Memory(k.device_region_at(PhysicalAddress::from_raw(UART)).unwrap()),
);
let outcome = k.handle_syscall(Syscall::RegionAddress { region: uart });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Value(UART))
));
}
#[test]
fn region_address_needs_read() {
let (mut k, init) = booted();
let pool = k
.create_region_cap(init, PageCount::new(1).unwrap())
.unwrap();
let blind = k
.mint(
init,
pool,
Badge::new(0),
PermissionSet::empty().with(Permission::Write),
)
.unwrap();
let e = error_of(k.handle_syscall(Syscall::RegionAddress { region: blind }));
assert!(matches!(e, KernelError::PermissionDenied { .. }));
}
#[test]
fn region_address_rejects_other_objects() {
let (mut k, _) = booted();
// An endpoint cap carries Read, so this fails on the object type.
let SyscallOutcome::Complete(SyscallResult::Slot(endpoint)) =
k.handle_syscall(Syscall::CreateEndpoint)
else {
panic!("expected an endpoint");
};
let e = error_of(k.handle_syscall(Syscall::RegionAddress { region: endpoint }));
assert!(matches!(e, KernelError::WrongObject));
}

View File

@@ -1,10 +1,9 @@
use super::*; use super::*;
use crate::{SyscallOutcome, SyscallResult}; use crate::{SyscallOutcome, SyscallResult};
use domain::resource::Resource;
use domain::syscall::Syscall; use domain::syscall::Syscall;
use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet};
use types::address::DataRef; use types::address::DataRef;
use types::bounded::CapSlot;
use types::permission::PermissionSet;
#[test] #[test]
fn delegate_without_delegate_permission_fails() { fn delegate_without_delegate_permission_fails() {
@@ -58,131 +57,16 @@ fn delegate_with_valid_cap_succeeds() {
// --- Generation-based revocation --- // --- Generation-based revocation ---
#[test] #[test]
fn revoke_resource_invalidates_all_caps() { fn every_representable_slot_is_rejected_not_panicking() {
let (mut k, pid) = kernel_with_process(); let (mut k, _) = kernel_with_process();
let ep_id = k.create_endpoint_for_test(); for raw in 1..=CapSlot::MAX {
let outcome = k.handle_syscall(Syscall::Send {
let cap_slot = k.grant_cap_for_test( endpoint: CapSlot::new(raw).unwrap(),
pid, message: domain::message::Message::empty(),
Resource::Endpoint(ep_id), });
PermissionSet::empty().with(Permission::Read), assert!(
); matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))),
"slot {raw} should be an error"
// Revoke the endpoint );
k.revoke_resource(&Resource::Endpoint(ep_id)); }
// Cap is now invalid — check_cap should reject with RevokedCapability
let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Error(domain::error::KernelError::RevokedCapability(_)))
));
}
#[test]
fn new_cap_after_revoke_uses_current_generation() {
let (mut k, pid) = kernel_with_process();
let ep_id = k.create_endpoint_for_test();
// Grant, revoke, grant again
let old_slot = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep_id),
PermissionSet::empty().with(Permission::Read),
);
k.revoke_resource(&Resource::Endpoint(ep_id));
// Old cap is dead
let outcome = k.handle_syscall(Syscall::Receive { endpoint: old_slot });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Error(domain::error::KernelError::RevokedCapability(_)))
));
// New cap with current generation works
let new_slot = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep_id),
PermissionSet::empty().with(Permission::Read),
);
let outcome = k.handle_syscall(Syscall::Receive { endpoint: new_slot });
// Should block (no sender), not error — proving the cap is valid
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
}
#[test]
fn revoke_does_not_affect_other_resources() {
let (mut k, pid) = kernel_with_process();
let ep1 = k.create_endpoint_for_test();
let ep2 = k.create_endpoint_for_test();
let slot1 = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep1),
PermissionSet::empty().with(Permission::Read),
);
let slot2 = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep2),
PermissionSet::empty().with(Permission::Read),
);
// Revoke only ep1
k.revoke_resource(&Resource::Endpoint(ep1));
// ep1 cap is revoked
let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot1 });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Error(domain::error::KernelError::RevokedCapability(_)))
));
// ep2 cap still works
let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot2 });
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
}
#[test]
fn revoke_syscall_invalidates_via_generation() {
let (mut k, pid) = kernel_with_process();
let ep_id = k.create_endpoint_for_test();
let cap_slot = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep_id),
PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Revoke),
);
// Revoke via syscall
let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot });
assert!(matches!(outcome, SyscallOutcome::Complete(SyscallResult::Ok)));
// The cap itself is now revoked (generation mismatch)
let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Error(domain::error::KernelError::RevokedCapability(_)))
));
}
#[test]
fn revoke_syscall_without_revoke_permission_fails() {
let (mut k, pid) = kernel_with_process();
let ep_id = k.create_endpoint_for_test();
let cap_slot = k.grant_cap_for_test(
pid,
Resource::Endpoint(ep_id),
PermissionSet::empty().with(Permission::Read),
);
let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot });
assert!(matches!(
outcome,
SyscallOutcome::Complete(SyscallResult::Error(_))
));
} }

View File

@@ -0,0 +1,326 @@
//! Randomized check of the derivation tree against an independent model.
//! The model applies the rules of docs/ipc.md (derive records a parent,
//! delete re-parents, revoke removes the subtree, death deletes everything
//! a process holds) to its own data; after every step the kernel must hold
//! exactly the capabilities the model says are alive.
extern crate std;
use std::collections::HashMap;
use std::vec::Vec;
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::syscall::Syscall;
use types::address::{DataRef, VirtualAddress};
use types::badge::Badge;
use types::bounded::{CapSlot, PageCount};
use types::id::ThreadId;
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
const STEPS: usize = 1500;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn pick<T: Copy>(&mut self, xs: &[T]) -> Option<T> {
(!xs.is_empty()).then(|| xs[self.below(xs.len())])
}
}
#[derive(Clone, Copy)]
struct Node {
parent: Option<usize>,
is_region: bool,
}
/// The reference model: logical capabilities and where each one lives.
#[derive(Default)]
struct Model {
nodes: Vec<Node>,
alive: HashMap<usize, (ProcessId, CapSlot)>,
}
impl Model {
fn add(&mut self, at: (ProcessId, CapSlot), parent: Option<usize>, is_region: bool) -> usize {
self.nodes.push(Node { parent, is_region });
let id = self.nodes.len() - 1;
self.alive.insert(id, at);
id
}
fn delete(&mut self, id: usize) {
let grandparent = self.nodes[id].parent;
for n in 0..self.nodes.len() {
if self.alive.contains_key(&n) && self.nodes[n].parent == Some(id) {
self.nodes[n].parent = grandparent;
}
}
self.alive.remove(&id);
}
fn is_descendant(&self, mut n: usize, root: usize) -> bool {
while let Some(p) = self.nodes[n].parent {
if p == root {
return true;
}
n = p;
}
false
}
fn revoke(&mut self, root: usize) {
let doomed: Vec<usize> = self
.alive
.keys()
.copied()
.filter(|&n| self.is_descendant(n, root))
.collect();
for n in doomed {
self.alive.remove(&n);
}
}
fn held_by(&self, pid: ProcessId) -> Vec<(usize, CapSlot)> {
self.alive
.iter()
.filter(|(_, (p, _))| *p == pid)
.map(|(id, (_, s))| (*id, *s))
.collect()
}
}
fn all() -> PermissionSet {
PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)
.with(Permission::Execute)
.with(Permission::Delegate)
}
struct World {
k: Kernel<MockPlatform>,
m: Model,
pids: [ProcessId; 3],
tids: [ThreadId; 3],
}
impl World {
fn new() -> Self {
let (k, a) = kernel_with_process();
let ta = k.current_thread().unwrap();
let mut w = Self {
k,
m: Model::default(),
pids: [a; 3],
tids: [ta; 3],
};
// init's own boot capability (ProcessCreate) is a kernel root.
let create = CapSlot::new(0).unwrap();
w.m.add((a, create), None, false);
for i in 1..3 {
w.spawn(i);
}
for call in [Syscall::CreateEndpoint, Syscall::CreateNotification] {
let s = w.slot(0, call);
w.m.add((a, s), None, false);
}
let r = w.slot(
0,
Syscall::CreateRegion {
pages: PageCount::new(1).unwrap(),
},
);
w.m.add((a, r), None, true);
w
}
fn slot(&mut self, who: usize, call: Syscall) -> CapSlot {
self.k.switch_to_thread_for_test(self.tids[who]);
match self.k.handle_syscall(call) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("expected a slot"),
}
}
/// (Re)creates process `i` from init; init gets a root process cap.
fn spawn(&mut self, i: usize) {
let cap = self.slot(
0,
Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
},
);
self.pids[i] = self.k.process_of_cap_for_test(cap);
self.tids[i] =
self.k
.spawn_thread_for_test(self.pids[i], Priority::DEFAULT, TimeSlice::DEFAULT);
self.m.add((self.pids[0], cap), None, false);
}
fn run(&mut self, who: usize, call: Syscall) -> bool {
self.k.switch_to_thread_for_test(self.tids[who]);
!matches!(
self.k.handle_syscall(call),
SyscallOutcome::Complete(SyscallResult::Error(_))
)
}
fn step(&mut self, rng: &mut Rng) {
let who = rng.below(3);
let pid = self.pids[who];
let held = self.m.held_by(pid);
let Some((id, slot)) = rng.pick(&held) else {
return;
};
match rng.below(10) {
0..=2 => {
// Badges only go on endpoints and notifications; the kernel
// must refuse the rest, so try both.
let badge = Badge::new(rng.below(2));
let before = self.k.cap_slots_for_test(pid);
let ok = self.run(
who,
Syscall::Mint {
slot,
badge,
permissions: all(),
},
);
if ok {
let new = newly_used(before, self.k.cap_slots_for_test(pid));
let is_region = self.m.nodes[id].is_region;
self.m.add((pid, new), Some(id), is_region);
}
}
3..=4 => {
// Delegate through any process cap the caller holds.
let proc_caps: Vec<(CapSlot, ProcessId)> = held
.iter()
.filter_map(|(_, s)| {
self.k.process_cap_target_for_test(pid, *s).map(|t| (*s, t))
})
.collect();
if let Some((to, target)) = rng.pick(&proc_caps) {
let before = self.k.cap_slots_for_test(target);
let ok = self.run(
who,
Syscall::Delegate {
slot,
to_endpoint: to,
permissions: all(),
},
);
if ok {
let new = newly_used(before, self.k.cap_slots_for_test(target));
let is_region = self.m.nodes[id].is_region;
self.m.add((target, new), Some(id), is_region);
}
}
}
// init keeps its ProcessCreate cap so processes can be re-made.
5 if !(who == 0 && slot == CapSlot::new(0).unwrap()) => {
if self.run(who, Syscall::DropCap { slot }) {
self.m.delete(id);
}
}
6..=7 => {
assert!(
self.run(who, Syscall::Revoke { slot }),
"revoking a held cap failed"
);
self.m.revoke(id);
}
8 => {
let va = VirtualAddress::from_raw(0x30000 + rng.below(16) * 0x1000);
let _ = self.run(
who,
Syscall::MapRegion {
region: slot,
va,
perms: PagePerms::ReadWrite,
},
);
}
_ if who == 0 && rng.below(4) == 0 => {
let victim = 1 + rng.below(2);
let vp = self.pids[victim];
// init's cap to the victim may already be revoked; kill it
// directly, as a supervisor holding a cap would.
for (n, _) in self.m.held_by(vp) {
self.m.delete(n);
}
self.k.destroy_process(vp);
self.spawn(victim);
}
_ => {}
}
}
fn check(&self) {
for &pid in &self.pids {
let used = self.k.cap_slots_for_test(pid);
let mut expected = [false; 64];
for (_, s) in self.m.held_by(pid) {
expected[s.value() as usize] = true;
}
assert_eq!(used, expected, "{pid:?} holds the wrong capabilities");
}
// Every capability-backed mapping is backed by a live capability
// of the mapping process.
for (_, region) in self.k.regions.iter() {
for m in region.mappings() {
if let Some(via) = m.via {
assert!(self.k.resolves(via), "mapping outlived its capability");
assert_eq!(via.process, m.process);
}
}
}
}
}
fn newly_used(before: [bool; 64], after: [bool; 64]) -> CapSlot {
(0..64)
.find(|&i| after[i] && !before[i])
.and_then(|i| CapSlot::new(i as u8))
.expect("a new slot")
}
#[test]
fn derivation_tree_matches_the_model() {
for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024] {
let mut world = World::new();
let mut rng = Rng(seed);
let mut revokes = 0;
for _ in 0..STEPS {
let before = world.m.alive.len();
world.step(&mut rng);
if world.m.alive.len() < before {
revokes += 1;
}
world.check();
}
assert!(
revokes > 20,
"seed {seed:#x}: too little deletion exercised"
);
assert!(
world.m.nodes.len() > 100,
"seed {seed:#x}: too little derivation"
);
}
}

View File

@@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::{SyscallOutcome, SyscallResult}; use crate::SyscallOutcome;
use domain::syscall::Syscall; use domain::syscall::Syscall;
#[test] #[test]
@@ -21,5 +21,5 @@ fn exit_thread_last_thread_destroys_process() {
let (mut k, pid) = kernel_with_process(); let (mut k, pid) = kernel_with_process();
k.handle_syscall(Syscall::ExitThread); k.handle_syscall(Syscall::ExitThread);
// Process should be gone since it had only one thread // Process should be gone since it had only one thread
assert!(k.process_exists(pid) == false); assert!(!k.process_exists(pid));
} }

View File

@@ -1,4 +1,3 @@
use domain::memory::page_pool::PagePool;
use domain::resource::Resource; use domain::resource::Resource;
use domain::scheduler::Scheduler; use domain::scheduler::Scheduler;
use domain::thread::ThreadState; use domain::thread::ThreadState;
@@ -6,25 +5,22 @@ use platform::Platform;
use types::{ use types::{
address::PhysicalAddress, address::PhysicalAddress,
bounded::{CapSlot, Priority}, bounded::{CapSlot, Priority},
id::{EndpointId, IrqId, ProcessId, ThreadId}, id::{EndpointId, ProcessId, ThreadId},
permission::PermissionSet, permission::PermissionSet,
time::TimeSlice, time::TimeSlice,
}; };
use crate::Kernel;
use super::mock_platform::MockPlatform; use super::mock_platform::MockPlatform;
use crate::Kernel;
pub fn test_kernel() -> Kernel<MockPlatform> { pub fn test_kernel() -> Kernel<MockPlatform> {
let mock = MockPlatform::new(1); let mock = MockPlatform::new(1);
let pool = PagePool::new(PhysicalAddress::from_raw(0x8000_0000), 1024); Kernel::new(mock, PhysicalAddress::from_raw(0x8400_0000), 1024)
Kernel::new(mock, pool)
} }
pub fn kernel_with_process() -> (Kernel<MockPlatform>, ProcessId) { pub fn kernel_with_process() -> (Kernel<MockPlatform>, ProcessId) {
let mut kernel = test_kernel(); let mut kernel = test_kernel();
let (pid, _tid) = kernel let (pid, _tid) = kernel.boot(Priority::DEFAULT, TimeSlice::DEFAULT).unwrap();
.boot(Priority::DEFAULT, TimeSlice::DEFAULT)
.unwrap();
(kernel, pid) (kernel, pid)
} }
@@ -35,22 +31,18 @@ impl Kernel<MockPlatform> {
priority: Priority, priority: Priority,
time_slice: TimeSlice, time_slice: TimeSlice,
) -> ThreadId { ) -> ThreadId {
let tid = self.threads.spawn(pid, priority, time_slice, None).expect("spawn"); let tid = self
.threads
.spawn(pid, priority, time_slice, None)
.expect("spawn");
let _ = self.processes.get_mut(pid).map(|p| p.add_thread(tid)); let _ = self.processes.get_mut(pid).map(|p| p.add_thread(tid));
let core = self.platform.core_id(); let core = self.platform.core_id();
self.set_home(tid, core);
self.scheduler.add(tid, priority, core); self.scheduler.add(tid, priority, core);
tid tid
} }
pub fn register_irq_for_test(&mut self, irq: IrqId, thread: ThreadId) { pub fn block_thread_for_test(&mut self, tid: ThreadId, reason: domain::thread::BlockReason) {
let _ = self.irqs.register(irq, thread);
}
pub fn block_thread_for_test(
&mut self,
tid: ThreadId,
reason: domain::thread::BlockReason,
) {
let core = self.platform.core_id(); let core = self.platform.core_id();
if let Ok(t) = self.threads.get_mut(tid) { if let Ok(t) = self.threads.get_mut(tid) {
if matches!(t.state(), ThreadState::Ready) { if matches!(t.state(), ThreadState::Ready) {
@@ -63,30 +55,44 @@ impl Kernel<MockPlatform> {
pub fn switch_to_thread_for_test(&mut self, tid: ThreadId) { pub fn switch_to_thread_for_test(&mut self, tid: ThreadId) {
let core = self.platform.core_id(); let core = self.platform.core_id();
assert_eq!(
self.home(tid),
core,
"a thread only ever runs on its home core"
);
// Only a thread ready in this core's queue can be switched to;
// otherwise leave the core as it is.
let switchable = self.scheduler.queued_on(tid) == Some(core)
&& self
.threads
.get(tid)
.is_ok_and(|t| matches!(t.state(), ThreadState::Ready));
if let Some(current) = self.scheduler.current(core) { if let Some(current) = self.scheduler.current(core) {
if current == tid { if current == tid || !switchable {
return; return;
} }
if let Ok(t) = self.threads.get_mut(current) { if let Ok(t) = self.threads.get_mut(current)
if matches!(t.state(), ThreadState::Running(_)) { && matches!(t.state(), ThreadState::Running(_))
let priority = t.priority(); {
let _ = t.preempt(); let priority = t.priority();
self.scheduler.add(current, priority, core); let _ = t.preempt();
} self.scheduler.add(current, priority, core);
} }
} }
if let Ok(t) = self.threads.get(tid) { if let Ok(t) = self.threads.get(tid)
if matches!(t.state(), ThreadState::Ready) { && matches!(t.state(), ThreadState::Ready)
self.scheduler.next(core); && self.scheduler.run_now(core, tid)
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core)); {
} let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
} }
} }
pub fn create_endpoint_for_test(&mut self) -> EndpointId { pub fn create_endpoint_for_test(&mut self) -> EndpointId {
self.endpoints.create().expect("create endpoint") self.endpoints
.create(ProcessId::from_raw(0))
.expect("create endpoint")
} }
pub fn grant_cap_for_test( pub fn grant_cap_for_test(
@@ -95,6 +101,95 @@ impl Kernel<MockPlatform> {
resource: Resource, resource: Resource,
permissions: PermissionSet, permissions: PermissionSet,
) -> CapSlot { ) -> CapSlot {
self.grant_cap(pid, resource, permissions).expect("grant cap") self.grant_cap(pid, resource, permissions)
.expect("grant cap")
}
pub fn endpoint_of_cap_for_test(&self, pid: ProcessId, slot: CapSlot) -> EndpointId {
match self
.processes
.get(pid)
.unwrap()
.capabilities()
.get(slot)
.unwrap()
.resource()
{
Resource::Endpoint(id) => *id,
_ => panic!("not an endpoint cap"),
}
}
/// The process a Process cap in init's table points at.
pub fn process_of_cap_for_test(&self, slot: CapSlot) -> ProcessId {
let init = ProcessId::from_raw(0);
match self
.processes
.get(init)
.unwrap()
.capabilities()
.get(slot)
.unwrap()
.resource()
{
Resource::Process(pid) => *pid,
_ => panic!("not a process cap"),
}
}
pub fn destroy_process_notifications_for_test(&mut self, owner: ProcessId) {
self.destroy_notifications_owned_by(owner);
}
/// The process a Process cap in `pid`'s table names, if it is one.
pub fn process_cap_target_for_test(&self, pid: ProcessId, slot: CapSlot) -> Option<ProcessId> {
match self
.processes
.get(pid)
.ok()?
.capabilities()
.get(slot)
.ok()?
.resource()
{
Resource::Process(target) if self.process_exists(*target) => Some(*target),
_ => None,
}
}
/// Whether `slot` of `pid` holds a capability whose object is gone.
pub fn is_dead_cap_for_test(&self, pid: ProcessId, slot: CapSlot) -> bool {
let Ok(p) = self.processes.get(pid) else {
return false;
};
p.capabilities()
.get(slot)
.is_ok_and(|c| self.resource_generation(c.resource()) != Some(*c.generation()))
}
/// Whether `slot` of `pid` holds a capability to a process that is gone.
pub fn is_dead_process_cap_for_test(&self, pid: ProcessId, slot: CapSlot) -> bool {
let Ok(p) = self.processes.get(pid) else {
return false;
};
match p.capabilities().get(slot).map(|c| *c.resource()) {
Ok(Resource::Process(target)) => !self.process_exists(target),
_ => false,
}
}
/// Which slots of `pid`'s capability table are occupied.
pub fn cap_slots_for_test(&self, pid: ProcessId) -> [bool; 64] {
let mut used = [false; 64];
if let Ok(p) = self.processes.get(pid) {
for (slot, _) in p.capabilities().iter() {
used[slot.value() as usize] = true;
}
}
used
}
pub fn thread_state_for_test(&self, tid: ThreadId) -> Option<ThreadState> {
self.threads.get(tid).ok().map(|t| *t.state())
} }
} }

View File

@@ -0,0 +1,299 @@
//! Randomized whole-kernel check of the memory model. Three processes issue
//! thousands of random (often hostile) memory operations; after every step
//! the kernel's records, the quotas and the mock "hardware" must agree.
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::memory::{RegionBacking, frame::FrameUse};
use domain::syscall::Syscall;
use platform::Platform;
use types::address::{DataRef, VirtualAddress};
use types::bounded::{CapSlot, PageCount};
use types::id::ThreadId;
use types::mapping::PagePerms;
const PAGE: usize = 4096;
const PROCS: usize = 3;
const STEPS: usize = 4000;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
// xorshift64
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
/// A VA that is usually plausible and sometimes plain hostile.
fn va(&mut self, known: &[usize]) -> usize {
match self.below(6) {
0 | 1 if !known.is_empty() => known[self.below(known.len())],
2 => self.below(0x0300_0000) & !(PAGE - 1),
3 => self.next() as usize,
4 => 0x8020_0000 + self.below(64) * PAGE,
_ => 0x10000 + self.below(64) * PAGE,
}
}
}
struct World {
k: Kernel<MockPlatform>,
pids: [ProcessId; PROCS],
tids: [ThreadId; PROCS],
alive: [bool; PROCS],
/// A's process caps for B and C.
caps: [CapSlot; PROCS],
known: [[usize; 64]; PROCS],
known_len: [usize; PROCS],
rebirths: usize,
}
impl World {
fn new() -> Self {
let (mut k, pid_a) = kernel_with_process();
let tid_a = k.current_thread().unwrap();
let mut pids = [pid_a; PROCS];
let mut tids = [tid_a; PROCS];
let mut caps = [CapSlot::new(0).unwrap(); PROCS];
for i in 1..PROCS {
caps[i] = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
};
pids[i] = ProcessId::from_raw(i as u16);
tids[i] = k.spawn_thread_for_test(pids[i], Priority::DEFAULT, TimeSlice::DEFAULT);
}
Self {
k,
pids,
tids,
alive: [true; PROCS],
caps,
known: [[0; 64]; PROCS],
known_len: [0; PROCS],
rebirths: 0,
}
}
fn remember(&mut self, who: usize, va: usize) {
let n = self.known_len[who];
self.known[who][n % 64] = va;
self.known_len[who] = (n + 1).min(64);
}
fn known(&self, who: usize) -> &[usize] {
&self.known[who][..self.known_len[who]]
}
fn step(&mut self, rng: &mut Rng) {
let who = rng.below(PROCS);
if !self.alive[who] {
return;
}
self.k.switch_to_thread_for_test(self.tids[who]);
let pid = self.pids[who];
let known: [usize; 64] = self.known[who];
let known = &known[..self.known_len[who]];
match rng.below(10) {
0..=2 => {
let n = 1 + rng.below(4) as u16;
if let SyscallOutcome::Complete(SyscallResult::Value(va)) =
self.k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(n).unwrap(),
})
{
for i in 0..n as usize {
self.remember(who, va + i * PAGE);
}
}
}
3..=4 => {
let addr = rng.va(known);
let count = PageCount::new(1 + rng.below(5) as u16).unwrap();
self.k.handle_syscall(Syscall::ReleasePages {
addr: VirtualAddress::from_raw(addr),
count,
});
}
5..=6 if who == 0 => {
let target = 1 + rng.below(PROCS - 1);
let src = rng.va(known);
let dst = rng.va(self.known(target));
let outcome = self.k.handle_syscall(Syscall::MapPage {
process: self.caps[target],
src_va: VirtualAddress::from_raw(src),
dst_va: VirtualAddress::from_raw(dst),
executable: rng.below(2) == 0,
});
if matches!(outcome, SyscallOutcome::Complete(SyscallResult::Ok)) {
self.remember(target, dst);
}
}
7 => {
let pages = PageCount::new(1 + rng.below(3) as u16).unwrap();
if let Ok(id) = self.k.create_pool_region(pid, pages) {
let other = rng.below(PROCS);
for p in [who, other] {
if self.alive[p] {
let va = VirtualAddress::from_raw(rng.va(self.known(p)));
let _ = self
.k
.map_region(id, self.pids[p], va, PagePerms::ReadWrite);
}
}
}
}
8 if who == 0 && rng.below(8) == 0 => {
let victim = 1 + rng.below(PROCS - 1);
let stale = self.caps[victim];
self.k.handle_syscall(Syscall::Kill { process: stale });
self.rebirths += 1;
// Re-create it; the pid slot is reused.
let fresh = match self.k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("re-create process"),
};
assert_eq!(self.k.process_of_cap_for_test(fresh), self.pids[victim]);
self.tids[victim] = self.k.spawn_thread_for_test(
self.pids[victim],
Priority::DEFAULT,
TimeSlice::DEFAULT,
);
self.known_len[victim] = 0;
// The old cap names the same pid but must reach nothing.
let src = rng.va(known);
let outcome = self.k.handle_syscall(Syscall::MapPage {
process: stale,
src_va: VirtualAddress::from_raw(src),
dst_va: VirtualAddress::from_raw(0x10000),
executable: false,
});
assert!(
matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))),
"stale process cap still works"
);
assert!(matches!(
self.k.handle_syscall(Syscall::Kill { process: stale }),
SyscallOutcome::Complete(SyscallResult::Error(_))
));
self.k.handle_syscall(Syscall::DropCap { slot: stale });
self.caps[victim] = fresh;
}
_ => {}
}
}
fn check(&self) {
let k = &self.k;
let window = MockPlatform::USER_VA_START..MockPlatform::USER_VA_END;
// Every frame is either free or charged to exactly one quota.
let charged: usize = self.pids.iter().map(|&p| k.page_usage(p)).sum();
assert_eq!(
charged + k.free_page_count(),
k.frames.capacity(),
"frames leaked or double counted"
);
for (i, &pid) in self.pids.iter().enumerate() {
let frames_owned = k
.frames
.iter()
.filter(|(_, u)| {
matches!(u, FrameUse::User { owner, .. } | FrameUse::PageTable { owner } if *owner == pid)
})
.count();
let region_pages: usize = k
.regions
.iter()
.filter(|(_, r)| r.owner() == pid && r.backing() == RegionBacking::Pool)
.map(|(_, r)| r.pages().value() as usize)
.sum();
assert_eq!(
k.page_usage(pid),
frames_owned + region_pages,
"quota of {pid:?} drifted"
);
if !self.alive[i] && i != 0 {
assert_eq!(k.page_usage(pid), 0);
assert_eq!(
k.platform.mapping_count(pid),
0,
"dead {pid:?} still mapped"
);
}
}
// Hardware -> kernel: every live mapping is one the kernel recorded.
for (pid, va, pa) in k.platform.mappings() {
assert!(
window.contains(&va),
"{pid:?} mapped outside window at {va:#x}"
);
let v = VirtualAddress::from_raw(va);
let as_user = k.frames.find_user(pid, v).map(|(p, _)| p.raw());
let as_region = k.regions.covering(pid, v).and_then(|id| {
let r = k.regions.get(id)?;
let m = r.mappings().find(|m| m.process == pid)?;
Some(r.base().raw() + (va - m.va.raw()))
});
assert!(
as_user == Some(pa) || as_region == Some(pa),
"{pid:?} has unrecorded mapping {va:#x} -> {pa:#x}"
);
}
// Kernel -> hardware: every recorded user page is really mapped.
for (pa, usage) in k.frames.iter() {
if let FrameUse::User { owner, page, .. } = usage {
assert_eq!(
k.platform.mapping(*owner, *page as usize * PAGE),
Some(pa.raw()),
"recorded page not mapped"
);
}
}
for (_, r) in k.regions.iter() {
for m in r.mappings() {
for i in 0..r.pages().value() as usize {
assert_eq!(
k.platform.mapping(m.process, m.va.raw() + i * PAGE),
Some(r.base().raw() + i * PAGE)
);
}
}
}
}
}
#[test]
fn memory_model_holds_under_random_hostile_operations() {
for seed in [1u64, 0xDEAD_BEEF, 0x1234_5678_9ABC, 42, 7777] {
let mut world = World::new();
let mut rng = Rng(seed);
for _ in 0..STEPS {
world.step(&mut rng);
world.check();
}
assert!(
world.rebirths >= 5,
"seed {seed:#x} barely exercised kill/re-create"
);
}
}

Some files were not shown because too many files have changed in this diff Show More