Compare commits
11 Commits
master
...
2e665fd66e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e665fd66e | |||
| b87a62475f | |||
| 55758ae5b2 | |||
| bafc18a5f8 | |||
| 130a82f50d | |||
| 154599e3af | |||
| 76f10ff1f1 | |||
| d43421e33e | |||
| 8d2c40cb1e | |||
| e931485ca7 | |||
| 1c04ddf909 |
@@ -1,3 +1,7 @@
|
||||
[target.riscv64gc-unknown-none-elf]
|
||||
runner = "qemu-system-riscv64 -machine virt -nographic -bios default -kernel"
|
||||
rustflags = ["-C", "panic=abort"]
|
||||
|
||||
[env]
|
||||
# Kernel tables are large fixed arrays; tests build `Kernel` by value.
|
||||
RUST_MIN_STACK = "33554432"
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -245,7 +245,6 @@ dependencies = [
|
||||
name = "platform"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"domain",
|
||||
"types",
|
||||
]
|
||||
|
||||
|
||||
@@ -123,3 +123,10 @@ panic = "abort"
|
||||
|
||||
[profile.dev]
|
||||
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
|
||||
|
||||
34
Makefile
34
Makefile
@@ -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
|
||||
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
|
||||
|
||||
# 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:
|
||||
cargo check --workspace
|
||||
|
||||
@@ -17,14 +34,17 @@ clippy:
|
||||
test:
|
||||
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:
|
||||
cargo kani -p types
|
||||
$(KANI) -p types
|
||||
|
||||
kani-domain:
|
||||
cargo kani -p domain
|
||||
$(KANI) -p domain
|
||||
|
||||
kani-kernel:
|
||||
cargo kani -p kernel
|
||||
$(KANI) -p kernel
|
||||
|
||||
kani: kani-types kani-domain kani-kernel
|
||||
|
||||
@@ -38,17 +58,17 @@ image: build
|
||||
scripts/make-disk-image.sh
|
||||
|
||||
run: build
|
||||
qemu-system-riscv64 -machine virt -nographic -bios default \
|
||||
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
|
||||
-kernel $(KERNEL_BIN)
|
||||
|
||||
run-disk: image
|
||||
qemu-system-riscv64 -machine virt -nographic -bios default \
|
||||
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
|
||||
-kernel $(KERNEL_BIN) \
|
||||
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
||||
-device virtio-blk-device,drive=hd0
|
||||
|
||||
run-gui: image
|
||||
qemu-system-riscv64 -machine virt -bios default \
|
||||
qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
|
||||
-kernel $(KERNEL_BIN) \
|
||||
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
||||
-device virtio-blk-device,drive=hd0 \
|
||||
|
||||
@@ -1,36 +1,60 @@
|
||||
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 {
|
||||
. = 0x80200000;
|
||||
. = PHYS_OFFSET + LOAD_ADDRESS;
|
||||
_text_start = .;
|
||||
|
||||
.text : {
|
||||
.text : AT(ADDR(.text) - PHYS_OFFSET) {
|
||||
*(.text.entry)
|
||||
*(.text .text.*)
|
||||
}
|
||||
|
||||
. = ALIGN(4096);
|
||||
.rodata : {
|
||||
_text_end = .;
|
||||
|
||||
.rodata : AT(ADDR(.rodata) - PHYS_OFFSET) {
|
||||
*(.rodata .rodata.*)
|
||||
*(.srodata .srodata.*)
|
||||
}
|
||||
.eh_frame : AT(ADDR(.eh_frame) - PHYS_OFFSET) {
|
||||
*(.eh_frame)
|
||||
}
|
||||
|
||||
. = ALIGN(4096);
|
||||
.data : {
|
||||
_rodata_end = .;
|
||||
|
||||
.data : AT(ADDR(.data) - PHYS_OFFSET) {
|
||||
*(.data .data.*)
|
||||
*(.sdata .sdata.*)
|
||||
}
|
||||
|
||||
. = ALIGN(4096);
|
||||
.bss : {
|
||||
.bss : AT(ADDR(.bss) - PHYS_OFFSET) {
|
||||
_bss_start = .;
|
||||
*(.bss .bss.*)
|
||||
*(.sbss .sbss.*)
|
||||
_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);
|
||||
_stack_bottom = .;
|
||||
. += 0x80000;
|
||||
_stack_top = .;
|
||||
_stacks_start = .;
|
||||
. += 8 * (4096 + 0x80000);
|
||||
_stacks_end = .;
|
||||
|
||||
_heap_start = .;
|
||||
}
|
||||
|
||||
_start_phys = _start - PHYS_OFFSET;
|
||||
|
||||
@@ -8,3 +8,29 @@ pub const CLINT_BASE: usize = 0x02000000;
|
||||
pub const USER_POOL_BASE: usize = 0x84000000;
|
||||
pub const USER_POOL_PAGES: usize = 4096;
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -22,37 +22,85 @@ pub fn grant_all(
|
||||
.with(types::permission::Permission::Read)
|
||||
.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"));
|
||||
|
||||
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"));
|
||||
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"));
|
||||
|
||||
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"));
|
||||
|
||||
let display_ep = kern().create_endpoint()
|
||||
let display_ep = kern()
|
||||
.create_endpoint(display_pid)
|
||||
.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"));
|
||||
|
||||
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"));
|
||||
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"));
|
||||
|
||||
let input_ep = kern().create_endpoint()
|
||||
let input_ep = kern()
|
||||
.create_endpoint(input_pid)
|
||||
.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"));
|
||||
|
||||
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"));
|
||||
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"));
|
||||
|
||||
uart.write_str("k-os: capabilities granted\n");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::hw::uart::Uart;
|
||||
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" {
|
||||
static _bss_start: u8;
|
||||
@@ -8,26 +8,66 @@ unsafe extern "C" {
|
||||
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();
|
||||
|
||||
let heap_start = unsafe { &_heap_start as *const u8 as usize };
|
||||
mmu::init_allocator(heap_start);
|
||||
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);
|
||||
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();
|
||||
crate::hw::timer::init();
|
||||
crate::hw::plic::Plic::init();
|
||||
uart.write_str("k-os: traps and timer initialized\n");
|
||||
|
||||
let riscv_platform = platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize);
|
||||
let pool = domain::memory::page_pool::PagePool::new(
|
||||
if board::USER_POOL_PAGES > domain::memory::frame::MAX_FRAMES {
|
||||
crate::halt(uart, "user pool larger than the frame table");
|
||||
}
|
||||
let riscv_platform =
|
||||
platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize, harts as u8);
|
||||
// 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.init(kernel::Kernel::new(riscv_platform, pool));
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
kernel_pt
|
||||
}
|
||||
|
||||
@@ -1,84 +1,74 @@
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::hw::virtio_blk::VirtioBlk;
|
||||
use crate::{boot_fs, mmu};
|
||||
use types::address::VirtualAddress;
|
||||
use types::id::{ProcessId, ThreadId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const PAGE_SIZE: usize = mmu::PAGE_SIZE;
|
||||
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;
|
||||
|
||||
pub struct LoadedProcess {
|
||||
pub pid: ProcessId,
|
||||
pub entry: usize,
|
||||
}
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
pub fn load_init(
|
||||
uart: &Uart,
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
) -> (LoadedProcess, ThreadId) {
|
||||
/// Scratch space for reading ELF files, allocated once from the boot heap.
|
||||
fn elf_buffer() -> &'static mut [u8] {
|
||||
static mut BUF: usize = 0;
|
||||
// SAFETY: boot is single threaded; the buffer is kernel-only memory
|
||||
// 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()
|
||||
.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"));
|
||||
|
||||
let entry = load_elf(blk, kernel_pt, pid, b"init.elf");
|
||||
if entry == 0 { crate::halt(uart, "init.elf not found"); }
|
||||
let entry = load_elf(uart, blk, pid, b"init.elf");
|
||||
setup_thread(tid, entry);
|
||||
uart.write_str("k-os: init loaded\n");
|
||||
|
||||
(LoadedProcess { pid, entry }, tid)
|
||||
(LoadedProcess { pid }, tid)
|
||||
}
|
||||
|
||||
pub fn load_server(
|
||||
uart: &Uart,
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
filename: &[u8],
|
||||
label: &str,
|
||||
) -> LoadedProcess {
|
||||
let pid = kern().create_process_internal()
|
||||
let pid = kern()
|
||||
.create_process_internal()
|
||||
.unwrap_or_else(|| crate::halt(uart, "process create failed"));
|
||||
let entry = load_elf(blk, kernel_pt, pid, filename);
|
||||
if entry == 0 { crate::halt(uart, "elf not found"); }
|
||||
let tid = kern().spawn_thread_internal(pid)
|
||||
let entry = load_elf(uart, blk, pid, filename);
|
||||
let tid = kern()
|
||||
.spawn_thread_internal(pid)
|
||||
.unwrap_or_else(|| crate::halt(uart, "thread spawn failed"));
|
||||
setup_thread(tid, entry);
|
||||
uart.write_str("k-os: ");
|
||||
uart.write_str(label);
|
||||
uart.write_str(" loaded\n");
|
||||
|
||||
LoadedProcess { pid, entry }
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
LoadedProcess { pid }
|
||||
}
|
||||
|
||||
fn setup_thread(tid: ThreadId, entry: usize) {
|
||||
@@ -89,88 +79,89 @@ fn setup_thread(tid: ThreadId, entry: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_elf(
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
pid: ProcessId,
|
||||
filename: &[u8],
|
||||
) -> usize {
|
||||
let buf_base = mmu::allocator::alloc_zeroed_page();
|
||||
let file_pages = MAX_FILE_SIZE / 4096;
|
||||
for _ in 1..file_pages {
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
}
|
||||
let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) };
|
||||
|
||||
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0);
|
||||
if file_size == 0 { return 0; }
|
||||
|
||||
let elf_info = match elf::parse(&buf[..file_size]) {
|
||||
Ok(info) => info,
|
||||
Err(_) => return 0,
|
||||
/// Copies `data` into `pid`'s already mapped pages starting at `va`.
|
||||
fn copy_to_user(pid: ProcessId, va: usize, data: &[u8]) -> bool {
|
||||
let mut done = 0;
|
||||
while done < data.len() {
|
||||
let addr = va + done;
|
||||
let page = addr & !(PAGE_SIZE - 1);
|
||||
let offset = addr - page;
|
||||
let Some(pa) = kern().user_page_frame(pid, VirtualAddress::from_raw(page)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
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() {
|
||||
let page_start = segment.vaddr & !0xFFF;
|
||||
let offset_in_page = segment.vaddr & 0xFFF;
|
||||
let total_size = offset_in_page + segment.mem_size;
|
||||
let seg_pages = (total_size + 4095) / 4096;
|
||||
|
||||
for p in 0..seg_pages {
|
||||
let page = mmu::allocator::alloc_zeroed_page();
|
||||
let page_va = page_start + p * 4096;
|
||||
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 src = segment.file_offset + seg_file_start;
|
||||
if src + copy_len <= file_size {
|
||||
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(
|
||||
buf.as_ptr().add(src),
|
||||
(page as *mut u8).add(dest_off),
|
||||
copy_len,
|
||||
data.as_ptr().add(done),
|
||||
(mmu::phys_to_virt(pa.raw()) as *mut u8).add(offset),
|
||||
len,
|
||||
);
|
||||
}
|
||||
done += len;
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
/// 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);
|
||||
if file_size == 0 {
|
||||
crate::halt(uart, "ELF not found");
|
||||
}
|
||||
let file = &buf[..file_size];
|
||||
let elf_info = elf::parse(file).unwrap_or_else(|_| crate::halt(uart, "bad ELF"));
|
||||
|
||||
for segment in elf_info.segments() {
|
||||
let perms =
|
||||
segment_perms(segment.flags).unwrap_or_else(|| crate::halt(uart, "W+X segment"));
|
||||
let page_start = segment.vaddr & !(PAGE_SIZE - 1);
|
||||
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;
|
||||
}
|
||||
|
||||
kern()
|
||||
.map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "cannot map segment"));
|
||||
|
||||
let copy_len = segment.file_size.min(segment.mem_size);
|
||||
let data = segment
|
||||
.file_offset
|
||||
.checked_add(copy_len)
|
||||
.and_then(|end| file.get(segment.file_offset..end))
|
||||
.unwrap_or_else(|| crate::halt(uart, "segment outside file"));
|
||||
if !copy_to_user(pid, segment.vaddr, data) {
|
||||
crate::halt(uart, "segment copy failed");
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..16usize {
|
||||
let sp = mmu::allocator::alloc_zeroed_page();
|
||||
mmu::map(user_pt, mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(USER_VA_BASE - (16 - i) * 4096),
|
||||
pa: types::address::PhysicalAddress::from_raw(sp),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
});
|
||||
}
|
||||
let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE;
|
||||
kern()
|
||||
.map_fresh_range(
|
||||
pid,
|
||||
VirtualAddress::from_raw(stack_bottom),
|
||||
USER_STACK_PAGES,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use crate::hw::virtio_blk::VirtioBlk;
|
||||
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
const MAX_FILE_SIZE: usize = 64 * 1024; // 64K max for init binary
|
||||
|
||||
struct BootFat16 {
|
||||
reserved_sectors: u16,
|
||||
sectors_per_fat: u16,
|
||||
root_dir_start: u32,
|
||||
root_dir_sectors: u32,
|
||||
data_start: u32,
|
||||
@@ -19,7 +17,9 @@ impl BootFat16 {
|
||||
}
|
||||
|
||||
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 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 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;
|
||||
|
||||
Some(Self {
|
||||
reserved_sectors,
|
||||
sectors_per_fat,
|
||||
root_dir_start,
|
||||
root_dir_sectors,
|
||||
data_start,
|
||||
@@ -50,7 +49,9 @@ impl BootFat16 {
|
||||
fn to_fat_name(name: &[u8]) -> [u8; 11] {
|
||||
let mut fat = [b' '; 11];
|
||||
let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len());
|
||||
for i in 0..dot_pos.min(8) { fat[i] = name[i].to_ascii_uppercase(); }
|
||||
for i in 0..dot_pos.min(8) {
|
||||
fat[i] = name[i].to_ascii_uppercase();
|
||||
}
|
||||
if dot_pos < name.len() {
|
||||
for i in 0..(name.len() - dot_pos - 1).min(3) {
|
||||
fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase();
|
||||
@@ -67,7 +68,9 @@ pub fn read_file_from_disk(
|
||||
let mut sector_buf = [0u8; SECTOR_SIZE];
|
||||
|
||||
// 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(§or_buf)?;
|
||||
|
||||
let fat_name = to_fat_name(filename);
|
||||
@@ -78,36 +81,52 @@ pub fn read_file_from_disk(
|
||||
let mut found = false;
|
||||
|
||||
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) {
|
||||
let off = i * 32;
|
||||
if sector_buf[off] == 0x00 { break; }
|
||||
if sector_buf[off] == 0xE5 { continue; }
|
||||
if sector_buf[off] == 0x00 {
|
||||
break;
|
||||
}
|
||||
if sector_buf[off] == 0xE5 {
|
||||
continue;
|
||||
}
|
||||
if sector_buf[off..off + 11] == fat_name {
|
||||
first_cluster = u16::from_le_bytes([sector_buf[off + 26], sector_buf[off + 27]]);
|
||||
file_size = u32::from_le_bytes([
|
||||
sector_buf[off + 28], sector_buf[off + 29],
|
||||
sector_buf[off + 30], sector_buf[off + 31],
|
||||
sector_buf[off + 28],
|
||||
sector_buf[off + 29],
|
||||
sector_buf[off + 30],
|
||||
sector_buf[off + 31],
|
||||
]);
|
||||
found = true;
|
||||
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 mut read = 0;
|
||||
let mut cluster = first_cluster;
|
||||
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 {
|
||||
if read >= size { break; }
|
||||
if read >= size {
|
||||
break;
|
||||
}
|
||||
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);
|
||||
out_buf[read..read + chunk].copy_from_slice(§or_buf[..chunk]);
|
||||
read += chunk;
|
||||
@@ -117,7 +136,9 @@ pub fn read_file_from_disk(
|
||||
let fat_offset = (cluster as u32) * 2;
|
||||
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;
|
||||
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]]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use super::{dma_buffer, info_page, kern, map_mmio};
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::hw::virtio_gpu::VirtioGpu;
|
||||
use crate::mmu;
|
||||
use types::display::Dimension;
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
const GPU_VA: usize = 0x20000;
|
||||
const VQUEUE_VA: usize = 0x21000;
|
||||
const CMDBUF_VA: usize = 0x23000;
|
||||
@@ -15,6 +11,8 @@ const PHYSINFO_VA: usize = 0x24000;
|
||||
const FB_VA: usize = 0x100000;
|
||||
const DEFAULT_WIDTH: Dimension = Dimension::new(640);
|
||||
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) {
|
||||
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);
|
||||
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
|
||||
kern()
|
||||
.set_page_limit(display_pid, DISPLAY_PAGE_LIMIT)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
|
||||
|
||||
map_page(pt, GPU_VA, gpu.base());
|
||||
|
||||
let vq_phys = alloc_contiguous(2);
|
||||
map_range(pt, VQUEUE_VA, vq_phys, 2);
|
||||
|
||||
let cmd_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, CMDBUF_VA, cmd_phys);
|
||||
map_mmio(uart, display_pid, GPU_VA, gpu.base(), 1);
|
||||
let vq_phys = dma_buffer(uart, display_pid, VQUEUE_VA, 2);
|
||||
let cmd_phys = dma_buffer(uart, display_pid, CMDBUF_VA, 1);
|
||||
|
||||
gpu.init_device(vq_phys);
|
||||
|
||||
let queue = hw_virtio::queue::VirtioQueue::new(
|
||||
hw_virtio::queue::QueueAddrs::from_base(vq_phys),
|
||||
);
|
||||
let queue = hw_virtio::queue::VirtioQueue::new(hw_virtio::queue::QueueAddrs::from_base(
|
||||
crate::mmu::phys_to_virt(vq_phys),
|
||||
));
|
||||
let dma = hw_virtio::gpu::GpuDmaAddrs {
|
||||
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),
|
||||
};
|
||||
use hw_virtio::mmio::MmioAccess;
|
||||
|
||||
let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) {
|
||||
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_phys = alloc_contiguous(fb_pages);
|
||||
map_range(pt, FB_VA, fb_phys, fb_pages);
|
||||
let fb_pages = (width.raw() as usize * height.raw() as usize * 4).div_ceil(4096);
|
||||
let fb_phys = dma_buffer(uart, display_pid, FB_VA, fb_pages);
|
||||
|
||||
let info_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, PHYSINFO_VA, info_phys);
|
||||
unsafe {
|
||||
let info = info_phys as *mut u64;
|
||||
core::ptr::write_volatile(info, cmd_phys as u64);
|
||||
core::ptr::write_volatile(info.add(1), (cmd_phys + 512) as u64);
|
||||
core::ptr::write_volatile(info.add(2), fb_phys as u64);
|
||||
core::ptr::write_volatile(info.add(3), width.raw() as u64);
|
||||
core::ptr::write_volatile(info.add(4), height.raw() as u64);
|
||||
}
|
||||
info_page(
|
||||
uart,
|
||||
display_pid,
|
||||
PHYSINFO_VA,
|
||||
&[
|
||||
cmd_phys as u64,
|
||||
(cmd_phys + 512) as u64,
|
||||
fb_phys as u64,
|
||||
width.raw() as u64,
|
||||
height.raw() as u64,
|
||||
],
|
||||
);
|
||||
|
||||
if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{dma_buffer, info_page, map_mmio};
|
||||
use crate::board;
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::{board, mmu};
|
||||
use types::id::ProcessId;
|
||||
|
||||
const UART_VA: usize = 0x20000;
|
||||
@@ -8,34 +9,23 @@ const INPUT_VQUEUE_VA: usize = 0x26000;
|
||||
const INPUT_EVBUF_VA: usize = 0x28000;
|
||||
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) {
|
||||
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
|
||||
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
|
||||
if let Some(mmio) = hw_virtio::mmio::scan_for_device(hw_virtio::mmio::DEVICE_ID_INPUT) {
|
||||
let base = mmio.base();
|
||||
map_page(pt, INPUT_MMIO_VA, base);
|
||||
|
||||
let vq_phys = alloc_contiguous(2);
|
||||
map_range(pt, INPUT_VQUEUE_VA, vq_phys, 2);
|
||||
|
||||
let evbuf_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, INPUT_EVBUF_VA, evbuf_phys);
|
||||
|
||||
let info_phys = mmu::allocator::alloc_zeroed_page();
|
||||
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);
|
||||
}
|
||||
let first = crate::mmu::phys_to_virt(hw_virtio::mmio::MMIO_FIRST);
|
||||
if let Some(mmio) = hw_virtio::mmio::scan_for_device(first, hw_virtio::mmio::DEVICE_ID_INPUT) {
|
||||
let base = crate::mmu::virt_to_phys(mmio.base());
|
||||
map_mmio(uart, input_pid, INPUT_MMIO_VA, base, 1);
|
||||
let vq_phys = dma_buffer(uart, input_pid, INPUT_VQUEUE_VA, 2);
|
||||
let evbuf_phys = dma_buffer(uart, input_pid, INPUT_EVBUF_VA, 1);
|
||||
info_page(
|
||||
uart,
|
||||
input_pid,
|
||||
INPUT_PHYSINFO_VA,
|
||||
&[evbuf_phys as u64, vq_phys as u64],
|
||||
);
|
||||
|
||||
hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,91 @@
|
||||
pub mod gpu_setup;
|
||||
pub mod input_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) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use super::{dma_buffer, info_page, map_mmio, shared_buffer};
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::mmu;
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
const VIRTIO_VA: usize = 0x20000;
|
||||
const VQUEUE_VA: usize = 0x21000;
|
||||
const REQHDR_VA: usize = 0x23000;
|
||||
@@ -13,102 +9,28 @@ const PHYSINFO_VA: usize = 0x24000;
|
||||
const SHARED_BUF_VA: usize = 0x30000;
|
||||
const SHARED_BUF_PAGES: usize = 16;
|
||||
|
||||
pub fn configure(
|
||||
uart: &Uart,
|
||||
init_pid: ProcessId,
|
||||
storage_pid: ProcessId,
|
||||
virtio_base: usize,
|
||||
) {
|
||||
let storage_pt = get_pt(storage_pid);
|
||||
let init_pt = get_pt(init_pid);
|
||||
|
||||
map_virtio_mmio(storage_pt, virtio_base);
|
||||
let vqueue_phys = alloc_and_map_virtqueue(storage_pt);
|
||||
let reqhdr_phys = alloc_and_map_reqhdr(storage_pt);
|
||||
write_phys_info(storage_pt, reqhdr_phys);
|
||||
pub fn configure(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId, virtio_base: usize) {
|
||||
map_mmio(uart, storage_pid, VIRTIO_VA, virtio_base, 1);
|
||||
let vqueue_phys = dma_buffer(uart, storage_pid, VQUEUE_VA, 2);
|
||||
let reqhdr_phys = dma_buffer(uart, storage_pid, REQHDR_VA, 1);
|
||||
info_page(
|
||||
uart,
|
||||
storage_pid,
|
||||
PHYSINFO_VA,
|
||||
&[
|
||||
reqhdr_phys as u64,
|
||||
(reqhdr_phys + 16) as u64,
|
||||
(reqhdr_phys + 32) as u64,
|
||||
],
|
||||
);
|
||||
reinit_device(virtio_base, vqueue_phys);
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
use crate::board;
|
||||
|
||||
const PLIC_PRIORITY: usize = board::PLIC_BASE;
|
||||
const PLIC_S_ENABLE: usize = board::PLIC_BASE + 0x2080;
|
||||
const PLIC_S_THRESHOLD: usize = board::PLIC_BASE + 0x201000;
|
||||
const PLIC_S_CLAIM: usize = board::PLIC_BASE + 0x201004;
|
||||
const PLIC: usize = crate::mmu::phys_to_virt(board::PLIC_BASE);
|
||||
const PLIC_PRIORITY: usize = PLIC;
|
||||
|
||||
// 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;
|
||||
|
||||
impl Plic {
|
||||
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) {
|
||||
let word = (irq / 32) as usize;
|
||||
let bit = irq % 32;
|
||||
let addr = PLIC_S_ENABLE + word * 4;
|
||||
let addr = enable_base() + word * 4;
|
||||
unsafe {
|
||||
let current = core::ptr::read_volatile(addr as *const u32);
|
||||
core::ptr::write_volatile(addr as *mut u32, current | (1 << bit));
|
||||
@@ -27,7 +43,7 @@ impl Plic {
|
||||
pub fn disable(irq: u32) {
|
||||
let word = (irq / 32) as usize;
|
||||
let bit = irq % 32;
|
||||
let addr = PLIC_S_ENABLE + word * 4;
|
||||
let addr = enable_base() + word * 4;
|
||||
unsafe {
|
||||
let current = core::ptr::read_volatile(addr as *const u32);
|
||||
core::ptr::write_volatile(addr as *mut u32, current & !(1 << bit));
|
||||
@@ -35,10 +51,10 @@ impl Plic {
|
||||
}
|
||||
|
||||
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) {
|
||||
unsafe { core::ptr::write_volatile(PLIC_S_CLAIM as *mut u32, irq) };
|
||||
unsafe { core::ptr::write_volatile(claim_reg() as *mut u32, irq) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ pub fn read_time() -> 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 {
|
||||
core::arch::asm!(
|
||||
"ecall",
|
||||
in("a0") stime,
|
||||
inlateout("a0") stime => _,
|
||||
lateout("a1") _,
|
||||
in("a6") 0usize,
|
||||
in("a7") 0x54494D45usize,
|
||||
);
|
||||
|
||||
@@ -6,7 +6,9 @@ pub struct Uart {
|
||||
|
||||
impl Uart {
|
||||
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) {
|
||||
@@ -30,7 +32,11 @@ impl Uart {
|
||||
let mut i = 0;
|
||||
while value > 0 {
|
||||
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;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use hw_virtio::mmio::{self, DirectMmio};
|
||||
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
|
||||
use crate::mmu::{phys_to_virt, virt_to_phys};
|
||||
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};
|
||||
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
@@ -11,7 +12,11 @@ struct VirtqMem {
|
||||
}
|
||||
|
||||
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 SECTOR_BUF: [u8; SECTOR_SIZE] = [0; SECTOR_SIZE];
|
||||
|
||||
@@ -23,29 +28,38 @@ pub struct VirtioBlk {
|
||||
|
||||
impl VirtioBlk {
|
||||
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 };
|
||||
mmio::init_legacy_device(&mmio, vq_phys, QUEUE_SIZE as u32);
|
||||
// The device sees physical addresses; the CPU uses kernel ones.
|
||||
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 {
|
||||
DmaAddrs {
|
||||
header_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_HEADER) as usize),
|
||||
header_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_HEADER) as usize),
|
||||
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),
|
||||
data_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(SECTOR_BUF) as usize),
|
||||
}
|
||||
let pair = |va: usize| {
|
||||
(
|
||||
PhysicalAddress::from_raw(virt_to_phys(va)),
|
||||
VirtualAddress::from_raw(va),
|
||||
)
|
||||
};
|
||||
let (header_pa, header_va) = pair(core::ptr::addr_of_mut!(REQ_HEADER) 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 })
|
||||
}
|
||||
|
||||
/// Physical address of the device's MMIO window.
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use hw_virtio::mmio::{self, DirectMmio};
|
||||
use hw_virtio::queue::{QueueAddrs, VirtioQueue, QUEUE_SIZE};
|
||||
use crate::mmu::{phys_to_virt, virt_to_phys};
|
||||
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::display::Dimension;
|
||||
|
||||
@@ -10,12 +11,13 @@ pub struct VirtioGpu {
|
||||
|
||||
impl VirtioGpu {
|
||||
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 })
|
||||
}
|
||||
|
||||
/// Physical address of the device's MMIO window.
|
||||
pub fn base(&self) -> usize {
|
||||
self.mmio.base()
|
||||
virt_to_phys(self.mmio.base())
|
||||
}
|
||||
|
||||
pub fn mmio(&self) -> &DirectMmio {
|
||||
@@ -26,15 +28,29 @@ impl VirtioGpu {
|
||||
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 {
|
||||
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys));
|
||||
pub fn setup_framebuffer(
|
||||
&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 {
|
||||
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),
|
||||
};
|
||||
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);
|
||||
ok1 && ok2 && ok3
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@ impl<T> KernelCell<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self, value: T) {
|
||||
unsafe { (*self.inner.get()).write(value) };
|
||||
/// Initializes the cell by letting `build` write the value straight
|
||||
/// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ mod hw;
|
||||
mod kernel_cell;
|
||||
mod mmu;
|
||||
mod platform;
|
||||
mod smp;
|
||||
mod trap;
|
||||
|
||||
use core::arch::global_asm;
|
||||
@@ -17,55 +18,168 @@ use kernel_cell::KernelCell;
|
||||
|
||||
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> {
|
||||
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!(
|
||||
r#"
|
||||
.section .text.entry
|
||||
.globl _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
|
||||
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:
|
||||
wfi
|
||||
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;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn kmain() -> ! {
|
||||
extern "C" fn kmain(hart: usize) -> ! {
|
||||
let uart = Uart::new();
|
||||
uart.write_str("k-os: booted on RISC-V\n");
|
||||
|
||||
let kernel_pt = boot::hardware::init(&uart);
|
||||
let mut blk = boot::hardware::probe_block_device(&uart)
|
||||
.unwrap_or_else(|| halt(&uart, "no block device"));
|
||||
boot::hardware::init(&uart, hart);
|
||||
// Held until the boot hart first enters userspace; harts started
|
||||
// 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 storage = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"storage.elf", "storage");
|
||||
let display = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"display.elf", "display");
|
||||
let input = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"input.elf", "input");
|
||||
let (init, init_thread) = boot::loader::load_init(&uart, &mut blk);
|
||||
if let Some(ctx) = kern_mut().thread_context_mut(init_thread) {
|
||||
ctx.registers_mut()[10] = SHARED_BUF_VA;
|
||||
}
|
||||
let 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();
|
||||
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::gpu_setup::configure(&uart, display.pid);
|
||||
devices::input_setup::configure(&uart, 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");
|
||||
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) -> ! {
|
||||
uart.write_str("k-os: FATAL — ");
|
||||
uart.write_str(msg);
|
||||
uart.put_char(b'\n');
|
||||
loop { unsafe { core::arch::asm!("wfi") }; }
|
||||
loop {
|
||||
unsafe { core::arch::asm!("wfi") };
|
||||
}
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
@@ -78,5 +192,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||
uart.write_dec(location.line() as usize);
|
||||
}
|
||||
uart.put_char(b'\n');
|
||||
loop { unsafe { core::arch::asm!("wfi") }; }
|
||||
loop {
|
||||
unsafe { core::arch::asm!("wfi") };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// Only used during init before the scheduler starts.
|
||||
// Boot-time bump allocator for kernel-only memory: the kernel page table
|
||||
// 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;
|
||||
|
||||
pub fn init_allocator(start: usize) {
|
||||
unsafe { NEXT_FREE = start; }
|
||||
unsafe {
|
||||
NEXT_FREE = start;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_zeroed_page() -> usize {
|
||||
unsafe {
|
||||
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;
|
||||
core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE);
|
||||
addr
|
||||
@@ -18,9 +25,5 @@ pub fn alloc_zeroed_page() -> usize {
|
||||
}
|
||||
|
||||
pub fn alloc_page_table() -> &'static mut PageTable {
|
||||
unsafe {
|
||||
let addr = alloc_zeroed_page();
|
||||
let table = &mut *(addr as *mut PageTable);
|
||||
table
|
||||
}
|
||||
unsafe { &mut *(alloc_zeroed_page() as *mut PageTable) }
|
||||
}
|
||||
|
||||
@@ -2,4 +2,24 @@ pub mod allocator;
|
||||
pub mod page_table;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::board;
|
||||
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::mapping::PagePerms;
|
||||
|
||||
pub use types::mapping::Mapping;
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
@@ -20,27 +24,30 @@ pub struct Sv39Flags {
|
||||
}
|
||||
|
||||
impl Sv39Flags {
|
||||
pub fn kernel_rwx() -> Self {
|
||||
Self { bits: PTE_R | PTE_W | PTE_X | PTE_G }
|
||||
pub fn kernel_rx() -> Self {
|
||||
Self {
|
||||
bits: PTE_R | PTE_X | PTE_G,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kernel_ro() -> Self {
|
||||
Self {
|
||||
bits: PTE_R | PTE_G,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Self { bits: PTE_R | PTE_X | PTE_U }
|
||||
}
|
||||
|
||||
pub fn user_rw() -> Self {
|
||||
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; }
|
||||
pub fn user(perms: PagePerms) -> Self {
|
||||
let bits = match perms {
|
||||
PagePerms::ReadOnly => PTE_R | PTE_U,
|
||||
PagePerms::ReadWrite => PTE_R | PTE_W | PTE_U,
|
||||
PagePerms::ReadExecute => PTE_R | PTE_X | PTE_U,
|
||||
};
|
||||
Self { bits }
|
||||
}
|
||||
|
||||
@@ -92,81 +99,223 @@ fn vpn(va: usize, level: usize) -> usize {
|
||||
(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) {
|
||||
let vpn2 = vpn(va, 2);
|
||||
let vpn1 = vpn(va, 1);
|
||||
let vpn0 = vpn(va, 0);
|
||||
|
||||
if !root.entries[vpn2].is_valid() {
|
||||
let table = alloc_page_table();
|
||||
let ppn = (table as *const PageTable as u64) >> 12;
|
||||
root.entries[vpn2] = PageTableEntry::new_branch(ppn);
|
||||
root.entries[vpn2] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
|
||||
}
|
||||
|
||||
let l1_addr = (root.entries[vpn2].ppn() << 12) as *mut PageTable;
|
||||
let l1 = unsafe { &mut *l1_addr };
|
||||
let l1 = table_at((root.entries[vpn2].ppn() << 12) as usize);
|
||||
|
||||
if !l1.entries[vpn1].is_valid() {
|
||||
let table = alloc_page_table();
|
||||
let ppn = (table as *const PageTable as u64) >> 12;
|
||||
l1.entries[vpn1] = PageTableEntry::new_branch(ppn);
|
||||
l1.entries[vpn1] = PageTableEntry::new_branch(table_ppn(alloc_page_table()));
|
||||
}
|
||||
|
||||
let l0_addr = (l1.entries[vpn1].ppn() << 12) as *mut PageTable;
|
||||
let l0 = unsafe { &mut *l0_addr };
|
||||
let l0 = table_at((l1.entries[vpn1].ppn() << 12) as usize);
|
||||
|
||||
let ppn = (pa as u64) >> 12;
|
||||
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>) {
|
||||
for i in 0..mapping.pages {
|
||||
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 {
|
||||
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);
|
||||
map(root, Mapping { va, pa, pages: board::RAM_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rwx() });
|
||||
// The kernel image, then the rest of RAM, each with only the access it
|
||||
// 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);
|
||||
map(root, Mapping { va, pa, pages: 1, flags: Sv39Flags::kernel_rw() });
|
||||
|
||||
let (va, pa) = identity(board::PLIC_BASE);
|
||||
map(root, Mapping { va, pa, pages: 0x400000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
|
||||
|
||||
let (va, pa) = identity(board::CLINT_BASE);
|
||||
map(root, Mapping { va, pa, pages: 0x10000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
|
||||
|
||||
let (va, pa) = identity(board::VIRTIO_BASE);
|
||||
map(root, Mapping { va, pa, pages: board::VIRTIO_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
|
||||
for (base, size) in [
|
||||
(board::UART_BASE, PAGE_SIZE),
|
||||
(board::PLIC_BASE, 0x40_0000),
|
||||
(board::CLINT_BASE, 0x1_0000),
|
||||
(board::VIRTIO_BASE, board::VIRTIO_SIZE),
|
||||
] {
|
||||
let (va, pa) = direct(base);
|
||||
map(
|
||||
root,
|
||||
Mapping {
|
||||
va,
|
||||
pa,
|
||||
pages: size / PAGE_SIZE,
|
||||
flags: Sv39Flags::kernel_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
let ppn = (root as *const PageTable as u64) >> 12;
|
||||
let satp = (8u64 << 60) | ppn;
|
||||
smp::note_active_root(virt_to_phys(root as *const PageTable as usize));
|
||||
let satp = (8u64 << 60) | table_ppn(root);
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"csrw satp, {0}",
|
||||
@@ -176,34 +325,105 @@ pub fn activate(root: &PageTable) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_user_page_table(
|
||||
// ---- user address spaces ----
|
||||
//
|
||||
// User mappings live in [USER_VA_START, USER_VA_END), which is root slot 0,
|
||||
// the L1 slots below USER_VA_END. Every address space gets its own root and its
|
||||
// own L1 for slot 0, and the kernel maps nothing in the lower half, so all
|
||||
// page tables below the user window are private to one process. The
|
||||
// kernel's upper-half entries are shared by copying them.
|
||||
|
||||
pub const USER_VA_START: usize = 0x1000;
|
||||
pub const USER_VA_END: usize = 0x0200_0000;
|
||||
|
||||
/// 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,
|
||||
_program_pa: PhysicalAddress,
|
||||
_program_pages: usize,
|
||||
_user_va: VirtualAddress,
|
||||
) -> &'static mut PageTable {
|
||||
let root = alloc_page_table();
|
||||
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 i in 0..512 {
|
||||
if !kernel_root.entries[i].is_valid() {
|
||||
continue;
|
||||
for (mine, kernels) in root.entries.iter_mut().zip(&kernel_root.entries).skip(256) {
|
||||
*mine = *kernels;
|
||||
}
|
||||
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];
|
||||
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);
|
||||
}
|
||||
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];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
root
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
use domain::memory::MemoryRegion;
|
||||
use domain::process::MAX_PROCESSES;
|
||||
use platform::Platform;
|
||||
use platform::{FrameSource, MapError, Platform};
|
||||
use types::{
|
||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
id::{CoreId, IrqId, ProcessId},
|
||||
permission::{Permission, PermissionSet},
|
||||
mapping::PagePerms,
|
||||
time::{Ticks, TimerDuration},
|
||||
};
|
||||
|
||||
use crate::board;
|
||||
use crate::hw::{plic::Plic, timer};
|
||||
use crate::mmu;
|
||||
use crate::smp;
|
||||
use crate::trap;
|
||||
|
||||
pub struct RiscVPlatform {
|
||||
num_cores: u8,
|
||||
kernel_pt: usize,
|
||||
/// Root table of each process's address space, 0 if it has none.
|
||||
page_tables: [usize; MAX_PROCESSES],
|
||||
}
|
||||
|
||||
impl RiscVPlatform {
|
||||
pub fn new(kernel_pt_addr: usize) -> Self {
|
||||
pub fn new(kernel_pt_addr: usize, num_cores: u8) -> Self {
|
||||
Self {
|
||||
num_cores: 1,
|
||||
num_cores,
|
||||
kernel_pt: kernel_pt_addr,
|
||||
page_tables: [0usize; MAX_PROCESSES],
|
||||
}
|
||||
@@ -33,37 +33,82 @@ impl RiscVPlatform {
|
||||
self.page_tables[pid.raw() as usize]
|
||||
}
|
||||
|
||||
pub fn set_page_table(&mut self, pid: ProcessId, addr: usize) {
|
||||
self.page_tables[pid.raw() as usize] = addr;
|
||||
fn root(&self, pid: ProcessId) -> Option<usize> {
|
||||
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 {
|
||||
fn configure_protection(&mut self, process: ProcessId, regions: &[MemoryRegion]) {
|
||||
let pt = self.ensure_page_table(process);
|
||||
for region in regions {
|
||||
mmu::map(pt, mmu::Mapping {
|
||||
va: VirtualAddress::from_raw(region.base().raw()),
|
||||
pa: region.base(),
|
||||
pages: region.size().value() as usize,
|
||||
flags: mmu::Sv39Flags::user(true, true, true),
|
||||
});
|
||||
const USER_VA_START: usize = mmu::USER_VA_START;
|
||||
const USER_VA_END: usize = mmu::USER_VA_END;
|
||||
|
||||
fn create_address_space(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> 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) -> ! {
|
||||
@@ -104,68 +149,34 @@ impl Platform for RiscVPlatform {
|
||||
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 {
|
||||
CoreId::from_raw(0)
|
||||
CoreId::from_raw(smp::hart_id() as u8)
|
||||
}
|
||||
|
||||
fn num_cores(&self) -> u8 {
|
||||
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) {
|
||||
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) -> ! {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ecall",
|
||||
in("a0") 0usize,
|
||||
in("a1") 0usize,
|
||||
inlateout("a0") 0usize => _,
|
||||
inlateout("a1") 0usize => _,
|
||||
in("a7") 0x53525354usize,
|
||||
in("a6") 0usize,
|
||||
);
|
||||
}
|
||||
loop { unsafe { core::arch::asm!("wfi") }; }
|
||||
loop {
|
||||
unsafe { core::arch::asm!("wfi") };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
220
crates/adapter-riscv/src/smp.rs
Normal file
220
crates/adapter-riscv/src/smp.rs
Normal 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
|
||||
}
|
||||
@@ -7,17 +7,9 @@ pub struct TrapFrame {
|
||||
pub sstatus: usize,
|
||||
}
|
||||
|
||||
impl TrapFrame {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
regs: [0; 32],
|
||||
sepc: 0,
|
||||
sstatus: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global_asm!(r#"
|
||||
global_asm!(
|
||||
r#"
|
||||
.section .text
|
||||
.align 4
|
||||
.globl trap_vector
|
||||
trap_vector:
|
||||
@@ -68,7 +60,14 @@ _from_user:
|
||||
|
||||
mv a0, sp
|
||||
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)
|
||||
csrw sepc, t0
|
||||
ld t0, 264(sp)
|
||||
@@ -84,7 +83,6 @@ _restore_smode:
|
||||
ld x1, 8(sp)
|
||||
ld x3, 24(sp)
|
||||
ld x4, 32(sp)
|
||||
ld x5, 40(sp)
|
||||
ld x6, 48(sp)
|
||||
ld x7, 56(sp)
|
||||
ld x8, 64(sp)
|
||||
@@ -112,27 +110,37 @@ _restore_smode:
|
||||
ld x30, 240(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
|
||||
andi t0, t0, 0x100
|
||||
bnez t0, _sret_smode
|
||||
ld x5, 40(sp)
|
||||
addi sp, sp, 272
|
||||
csrrw sp, sscratch, sp
|
||||
_sret_smode:
|
||||
sret
|
||||
"#);
|
||||
_sret_smode:
|
||||
ld x5, 40(sp)
|
||||
addi sp, sp, 272
|
||||
sret
|
||||
"#
|
||||
);
|
||||
|
||||
const CAUSE_INTERRUPT_BIT: usize = 1 << 63;
|
||||
const CAUSE_S_SOFTWARE: usize = 1;
|
||||
const CAUSE_S_TIMER: usize = 5;
|
||||
const CAUSE_S_EXTERNAL: usize = 9;
|
||||
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() {
|
||||
unsafe extern "C" {
|
||||
fn trap_vector();
|
||||
}
|
||||
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 sscratch, zero");
|
||||
// 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> {
|
||||
use domain::message::Message;
|
||||
use domain::syscall::{Syscall, WaitSet};
|
||||
use types::syscall_abi::*;
|
||||
|
||||
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 args(frame: &TrapFrame) -> [usize; 6] {
|
||||
let mut a = [0usize; 6];
|
||||
a.copy_from_slice(&frame.regs[10..16]);
|
||||
a
|
||||
}
|
||||
|
||||
fn write_result(frame: &mut TrapFrame, outcome: &kernel::SyscallOutcome) {
|
||||
use kernel::{SyscallOutcome, SyscallResult};
|
||||
fn set_results(frame: &mut TrapFrame, regs: [usize; 8]) {
|
||||
frame.regs[10..18].copy_from_slice(®s);
|
||||
}
|
||||
|
||||
match outcome {
|
||||
SyscallOutcome::Complete(result) => 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,
|
||||
},
|
||||
SyscallOutcome::BlockCaller(_) => {}
|
||||
SyscallOutcome::SwitchTo(_) => frame.regs[10] = 0,
|
||||
SyscallOutcome::TerminateCaller => {}
|
||||
}
|
||||
fn write_result(frame: &mut TrapFrame, result: &kernel::SyscallResult) {
|
||||
set_results(frame, kernel::abi::encode(result));
|
||||
}
|
||||
|
||||
fn save_frame_to_context(frame: &TrapFrame, ctx: &mut domain::thread::SavedContext) {
|
||||
@@ -292,88 +185,136 @@ fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFra
|
||||
}
|
||||
|
||||
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) {
|
||||
write_result_direct(frame, &result);
|
||||
if let Some(result) = kern().take_pending_result(tid) {
|
||||
write_result(frame, &result);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_result_direct(frame: &mut TrapFrame, result: &kernel::SyscallResult) {
|
||||
use kernel::SyscallResult;
|
||||
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,
|
||||
}
|
||||
unsafe extern "C" {
|
||||
fn trap_return(frame: *mut TrapFrame) -> !;
|
||||
}
|
||||
|
||||
fn switch_to_thread(frame: &mut TrapFrame, current_tid: types::id::ThreadId, next_tid: types::id::ThreadId) {
|
||||
let kern = unsafe { &mut *crate::kernel_ptr() };
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if let Some(ctx) = kern.thread_context(to) {
|
||||
load_context_to_frame(ctx, frame);
|
||||
}
|
||||
|
||||
apply_pending_result(frame, next_tid);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
apply_pending_result(frame, to);
|
||||
}
|
||||
|
||||
fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
|
||||
let kern = unsafe { &mut *crate::kernel_ptr() };
|
||||
|
||||
if let Some(ctx) = kern.thread_context_mut(current_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);
|
||||
}
|
||||
|
||||
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);
|
||||
let next = next_runnable();
|
||||
let _ = kern().set_thread_running(next, this_core());
|
||||
switch_to_thread(frame, None, next);
|
||||
}
|
||||
|
||||
apply_pending_result(frame, next_tid);
|
||||
|
||||
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);
|
||||
/// The next thread to run on this hart, idling until one is ready.
|
||||
///
|
||||
/// Idling gives up the kernel lock: an idle hart holds nothing, so other
|
||||
/// harts can make work for it (and kick it) meanwhile. It also leaves any
|
||||
/// user address space first, so nobody needs to shoot it down. SIE stays
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loop { unsafe { core::arch::asm!("wfi") }; }
|
||||
|
||||
/// Claims every pending PLIC interrupt and hands each to the kernel. Only
|
||||
/// the boot hart receives external interrupts.
|
||||
fn dispatch_external() {
|
||||
loop {
|
||||
let irq_num = crate::hw::plic::Plic::claim();
|
||||
if irq_num == 0 {
|
||||
return;
|
||||
}
|
||||
kern().handle_interrupt(types::id::IrqId::from_raw(irq_num as u16));
|
||||
crate::hw::plic::Plic::complete(irq_num);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
@@ -381,68 +322,83 @@ extern "C" fn trap_handler_rust(frame: &mut TrapFrame) {
|
||||
let scause: usize;
|
||||
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 {
|
||||
handle_interrupt(frame, scause & !CAUSE_INTERRUPT_BIT);
|
||||
} else {
|
||||
handle_exception(frame, scause);
|
||||
}
|
||||
finish(frame);
|
||||
crate::smp::unlock();
|
||||
}
|
||||
|
||||
fn handle_interrupt(frame: &mut TrapFrame, cause: usize) {
|
||||
match cause {
|
||||
CAUSE_S_TIMER => {
|
||||
crate::hw::timer::handle_interrupt();
|
||||
let kern = unsafe { &mut *crate::kernel_ptr() };
|
||||
let core = types::id::CoreId::from_raw(0);
|
||||
let kern = kern();
|
||||
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 {
|
||||
switch_to_thread(frame, current_tid, next);
|
||||
switch_to_thread(frame, Some(current_tid), next);
|
||||
}
|
||||
}
|
||||
}
|
||||
CAUSE_S_EXTERNAL => {
|
||||
let irq_num = crate::hw::plic::Plic::claim();
|
||||
if irq_num != 0 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
CAUSE_S_EXTERNAL => dispatch_external(),
|
||||
// Serviced on entry; any reschedule happens in `finish`.
|
||||
CAUSE_S_SOFTWARE => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
match scause {
|
||||
CAUSE_ECALL_U => {
|
||||
frame.sepc += 4;
|
||||
|
||||
if let Some(syscall) = decode_syscall(frame) {
|
||||
let kern = unsafe { &mut *crate::kernel_ptr() };
|
||||
let Some(syscall) = kernel::abi::decode(frame.regs[17], &args(frame)) else {
|
||||
set_results(frame, kernel::abi::unknown_syscall());
|
||||
return;
|
||||
};
|
||||
{
|
||||
let kern = kern();
|
||||
let current_tid = kern.current_thread();
|
||||
let outcome = kern.handle_syscall(syscall);
|
||||
write_result(frame, &outcome);
|
||||
|
||||
match outcome {
|
||||
kernel::SyscallOutcome::Complete(_) => {}
|
||||
kernel::SyscallOutcome::BlockCaller(_) => {
|
||||
if let Some(tid) = current_tid {
|
||||
switch_to_next(frame, tid);
|
||||
}
|
||||
}
|
||||
kernel::SyscallOutcome::Complete(result) => write_result(frame, &result),
|
||||
kernel::SyscallOutcome::BlockCaller(_) => switch_to_next(frame, current_tid),
|
||||
kernel::SyscallOutcome::SwitchTo(next) => {
|
||||
if let Some(tid) = current_tid {
|
||||
switch_to_thread(frame, tid, next);
|
||||
}
|
||||
switch_to_thread(frame, current_tid, next)
|
||||
}
|
||||
kernel::SyscallOutcome::TerminateCaller => {
|
||||
let uart = crate::hw::uart::Uart::new();
|
||||
uart.write_str("k-os: process exited\n");
|
||||
if let Some(tid) = current_tid {
|
||||
switch_to_next(frame, tid);
|
||||
}
|
||||
switch_to_next(frame, current_tid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,7 +415,7 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
|
||||
uart.write_hex(frame.sepc);
|
||||
uart.put_char(b'\n');
|
||||
|
||||
let kern = unsafe { &mut *crate::kernel_ptr() };
|
||||
let kern = kern();
|
||||
if let Some(current_tid) = kern.current_thread() {
|
||||
let fault = match scause {
|
||||
2 => domain::fault::Fault::IllegalInstruction,
|
||||
@@ -469,10 +425,10 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) {
|
||||
_ => domain::fault::Fault::InvalidSyscall,
|
||||
};
|
||||
kern.handle_fault(current_tid, fault, frame.sepc);
|
||||
switch_to_next(frame, current_tid);
|
||||
} else {
|
||||
loop { unsafe { core::arch::asm!("wfi") }; }
|
||||
}
|
||||
switch_to_next(frame, Some(current_tid));
|
||||
}
|
||||
// With no current thread (another hart just destroyed it),
|
||||
// `finish` moves on to the next one.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const CELL_SIZE: u16 = 24;
|
||||
const BG_COLOR: u32 = 0xFF202040;
|
||||
const SNAKE_COLOR: u32 = 0xFF00FF00;
|
||||
const HEAD_COLOR: u32 = 0xFF00CC00;
|
||||
const FOOD_COLOR: u32 = 0xFFFF0000;
|
||||
pub(crate) const FOOD_COLOR: u32 = 0xFFFF0000;
|
||||
const BORDER_COLOR: u32 = 0xFF505080;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
||||
@@ -28,5 +28,5 @@ fn food_is_rendered() {
|
||||
let count = render(&snake, &mut commands);
|
||||
// Food is command index 2 (after bg and border)
|
||||
assert!(count > 2);
|
||||
assert_eq!(commands[2].color, 0xFF0040);
|
||||
assert_eq!(commands[2].color, crate::renderer::FOOD_COLOR);
|
||||
}
|
||||
|
||||
@@ -5,3 +5,6 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
|
||||
|
||||
88
crates/domain/src/capability/derivation.rs
Normal file
88
crates/domain/src/capability/derivation.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
40
crates/domain/src/capability/derivation_tests.rs
Normal file
40
crates/domain/src/capability/derivation_tests.rs
Normal 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]);
|
||||
}
|
||||
@@ -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 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)]
|
||||
#[path = "tests.rs"]
|
||||
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 {
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
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 {
|
||||
pub fn new(
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
generation: Generation,
|
||||
) -> Self {
|
||||
pub fn new(resource: Resource, permissions: PermissionSet, generation: Generation) -> Self {
|
||||
Self {
|
||||
resource,
|
||||
permissions,
|
||||
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 {
|
||||
&self.resource
|
||||
}
|
||||
@@ -44,39 +90,71 @@ impl Capability {
|
||||
|
||||
pub struct CapabilityTable {
|
||||
slots: [Option<Capability>; MAX_CAPS_PER_PROCESS],
|
||||
generations: SlotGenerations<MAX_CAPS_PER_PROCESS>,
|
||||
}
|
||||
|
||||
impl CapabilityTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slots: [const { None }; MAX_CAPS_PER_PROCESS],
|
||||
generations: SlotGenerations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
|
||||
let idx = slot.value() as usize;
|
||||
self.slots[idx]
|
||||
.as_ref()
|
||||
fn slot(&self, slot: CapSlot) -> Option<&Capability> {
|
||||
self.slots.get(slot.value() as usize)?.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))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> {
|
||||
let idx = self
|
||||
.slots
|
||||
.iter()
|
||||
.position(|s| s.is_none())
|
||||
.ok_or(KernelError::NoFreeSlot)?;
|
||||
pub fn iter(&self) -> impl Iterator<Item = (CapSlot, &Capability)> {
|
||||
self.slots.iter().enumerate().filter_map(|(i, c)| {
|
||||
let cap = c.as_ref()?;
|
||||
Some((CapSlot::new(i as u8)?, cap))
|
||||
})
|
||||
}
|
||||
|
||||
self.slots[idx] = Some(cap);
|
||||
CapSlot::new(idx as u8).ok_or(KernelError::NoFreeSlot)
|
||||
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
|
||||
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> {
|
||||
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));
|
||||
}
|
||||
self.slots[idx] = None;
|
||||
self.generations.advance(idx);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CapabilityTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() {
|
||||
#[test]
|
||||
fn generation_mismatch_detected() {
|
||||
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_v2));
|
||||
@@ -109,7 +109,6 @@ fn restrict_to_cannot_add_permissions() {
|
||||
Permission::Write,
|
||||
Permission::Execute,
|
||||
Permission::Delegate,
|
||||
Permission::Revoke,
|
||||
]);
|
||||
|
||||
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::Execute));
|
||||
assert!(!result.allows(Permission::Delegate));
|
||||
assert!(!result.allows(Permission::Revoke));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,7 +136,6 @@ fn empty_permission_set_allows_nothing() {
|
||||
assert!(!empty.allows(Permission::Write));
|
||||
assert!(!empty.allows(Permission::Execute));
|
||||
assert!(!empty.allows(Permission::Delegate));
|
||||
assert!(!empty.allows(Permission::Revoke));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,3 +147,45 @@ fn capability_preserves_resource_and_permissions() {
|
||||
assert_eq!(cap.permissions(), &expected_permissions);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use types::{
|
||||
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;
|
||||
|
||||
@@ -14,18 +17,19 @@ mod tests;
|
||||
|
||||
pub struct Endpoint {
|
||||
id: EndpointId,
|
||||
/// The process that created it; the endpoint dies with its owner.
|
||||
owner: ProcessId,
|
||||
waiting_senders: WaitQueue,
|
||||
waiting_receivers: WaitQueue,
|
||||
generation: Generation,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
pub fn new(id: EndpointId) -> Self {
|
||||
pub fn new(id: EndpointId, owner: ProcessId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
owner,
|
||||
waiting_senders: WaitQueue::new(),
|
||||
waiting_receivers: WaitQueue::new(),
|
||||
generation: Generation::initial(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +37,8 @@ impl Endpoint {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> &Generation {
|
||||
&self.generation
|
||||
pub fn owner(&self) -> ProcessId {
|
||||
self.owner
|
||||
}
|
||||
|
||||
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
|
||||
@@ -53,6 +57,22 @@ impl Endpoint {
|
||||
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 {
|
||||
!self.waiting_senders.is_empty()
|
||||
}
|
||||
@@ -68,52 +88,85 @@ impl Endpoint {
|
||||
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
|
||||
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 {
|
||||
endpoints: [Option<Endpoint>; MAX_ENDPOINTS],
|
||||
generations: SlotGenerations<MAX_ENDPOINTS>,
|
||||
}
|
||||
|
||||
impl EndpointTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
endpoints: [const { None }; MAX_ENDPOINTS],
|
||||
generations: SlotGenerations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self) -> Option<EndpointId> {
|
||||
let idx = self.endpoints.iter().position(|s| s.is_none())?;
|
||||
pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
|
||||
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);
|
||||
self.endpoints[idx] = Some(Endpoint::new(id));
|
||||
*slot = Some(Endpoint::new(id, owner));
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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) {
|
||||
for slot in &mut self.endpoints {
|
||||
if let Some(ep) = slot {
|
||||
for ep in self.endpoints.iter_mut().flatten() {
|
||||
ep.remove_sender(thread);
|
||||
ep.remove_receiver(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EndpointTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MessagePassing {
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
use crate::endpoint::wait_queue::MAX_WAITERS;
|
||||
|
||||
use super::*;
|
||||
use types::id::{EndpointId, ThreadId};
|
||||
use types::id::{EndpointId, ProcessId, ThreadId};
|
||||
|
||||
fn thread(id: u16) -> ThreadId {
|
||||
ThreadId::from_raw(id)
|
||||
}
|
||||
|
||||
fn owner() -> ProcessId {
|
||||
ProcessId::from_raw(0)
|
||||
}
|
||||
|
||||
fn test_endpoint() -> Endpoint {
|
||||
Endpoint::new(EndpointId::from_raw(1))
|
||||
Endpoint::new(EndpointId::from_raw(1), owner())
|
||||
}
|
||||
|
||||
// --- WaitQueue tests ---
|
||||
|
||||
#[test]
|
||||
fn empty_queue_pop_returns_none() {
|
||||
let mut queue = WaitQueue::new();
|
||||
let mut queue: WaitQueue = WaitQueue::new();
|
||||
assert!(queue.pop().is_none());
|
||||
assert!(queue.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_then_pop_returns_same_thread() {
|
||||
let mut queue = WaitQueue::new();
|
||||
let mut queue: WaitQueue = WaitQueue::new();
|
||||
queue.push(thread(1)).unwrap();
|
||||
assert_eq!(queue.pop(), Some(thread(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_order_is_fifo() {
|
||||
let mut queue = WaitQueue::new();
|
||||
let mut queue: WaitQueue = WaitQueue::new();
|
||||
queue.push(thread(1)).unwrap();
|
||||
queue.push(thread(2)).unwrap();
|
||||
queue.push(thread(3)).unwrap();
|
||||
@@ -42,7 +46,7 @@ fn pop_order_is_fifo() {
|
||||
|
||||
#[test]
|
||||
fn full_queue_rejects_push() {
|
||||
let mut queue = WaitQueue::new();
|
||||
let mut queue: WaitQueue = WaitQueue::new();
|
||||
for i in 0..MAX_WAITERS {
|
||||
assert!(queue.push(thread(i as u16)).is_some());
|
||||
}
|
||||
@@ -51,7 +55,7 @@ fn full_queue_rejects_push() {
|
||||
|
||||
#[test]
|
||||
fn is_empty_after_drain() {
|
||||
let mut queue = WaitQueue::new();
|
||||
let mut queue: WaitQueue = WaitQueue::new();
|
||||
queue.push(thread(1)).unwrap();
|
||||
queue.push(thread(2)).unwrap();
|
||||
queue.pop();
|
||||
@@ -99,13 +103,34 @@ fn senders_and_receivers_are_independent() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_bumps_generation() {
|
||||
let mut ep = test_endpoint();
|
||||
let gen_before = *ep.generation();
|
||||
ep.revoke();
|
||||
let gen_after = *ep.generation();
|
||||
fn reused_slot_gets_a_new_generation() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create(owner()).unwrap();
|
||||
let old = table.generation(id).unwrap();
|
||||
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]
|
||||
@@ -141,22 +166,22 @@ fn rendezvous_receiver_first() {
|
||||
#[test]
|
||||
fn table_create_and_lookup() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create().unwrap();
|
||||
let id = table.create(owner()).unwrap();
|
||||
assert!(table.get(id).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_create_returns_unique_ids() {
|
||||
let mut table = EndpointTable::new();
|
||||
let a = table.create().unwrap();
|
||||
let b = table.create().unwrap();
|
||||
let a = table.create(owner()).unwrap();
|
||||
let b = table.create(owner()).unwrap();
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_destroy_then_get_fails() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create().unwrap();
|
||||
let id = table.create(owner()).unwrap();
|
||||
let removed = table.destroy(id);
|
||||
assert!(removed.is_some());
|
||||
assert!(table.get(id).is_none());
|
||||
@@ -165,16 +190,57 @@ fn table_destroy_then_get_fails() {
|
||||
#[test]
|
||||
fn table_get_mut_allows_enqueue() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create().unwrap();
|
||||
table.get_mut(id).unwrap().enqueue_sender(thread(1)).unwrap();
|
||||
let id = table.create(owner()).unwrap();
|
||||
table
|
||||
.get_mut(id)
|
||||
.unwrap()
|
||||
.enqueue_sender(thread(1))
|
||||
.unwrap();
|
||||
assert!(table.get(id).unwrap().has_waiting_sender());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_destroy_reuses_slot() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create().unwrap();
|
||||
let id = table.create(owner()).unwrap();
|
||||
table.destroy(id);
|
||||
let new_id = table.create().unwrap();
|
||||
let new_id = table.create(owner()).unwrap();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,141 @@
|
||||
use types::id::ThreadId;
|
||||
|
||||
pub const MAX_WAITERS: usize = 16;
|
||||
pub const MAX_WAITERS: usize = if cfg!(kani) { 2 } else { 16 };
|
||||
|
||||
pub struct WaitQueue {
|
||||
entries: [Option<ThreadId>; MAX_WAITERS],
|
||||
count: u8,
|
||||
/// Bounded FIFO of blocked threads. Order is preserved across pops and
|
||||
/// removals, so no waiter can be overtaken.
|
||||
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 {
|
||||
Self {
|
||||
entries: [None; MAX_WAITERS],
|
||||
count: 0,
|
||||
entries: [None; N],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, thread: ThreadId) -> Option<()> {
|
||||
let slot = self.entries.iter_mut().find(|s| s.is_none())?;
|
||||
*slot = Some(thread);
|
||||
self.count += 1;
|
||||
if self.len == N || self.contains(thread) {
|
||||
return None;
|
||||
}
|
||||
*self.entries.get_mut(self.len)? = Some(thread);
|
||||
self.len += 1;
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<ThreadId> {
|
||||
let slot = self.entries.iter_mut().find(|s| s.is_some())?;
|
||||
let thread = slot.take();
|
||||
self.count -= 1;
|
||||
thread
|
||||
let head = self.entries.first().copied().flatten()?;
|
||||
self.close_gap(0);
|
||||
Some(head)
|
||||
}
|
||||
|
||||
pub fn remove_thread(&mut self, thread: ThreadId) -> bool {
|
||||
for slot in &mut self.entries {
|
||||
if *slot == Some(thread) {
|
||||
*slot = None;
|
||||
self.count -= 1;
|
||||
return true;
|
||||
match self.entries[..self.len]
|
||||
.iter()
|
||||
.position(|e| *e == Some(thread))
|
||||
{
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ use types::{
|
||||
pub enum KernelError {
|
||||
InvalidSlot(CapSlot),
|
||||
RevokedCapability(CapSlot),
|
||||
PermissionDenied { slot: CapSlot, needed: Permission },
|
||||
PermissionDenied {
|
||||
slot: CapSlot,
|
||||
needed: Permission,
|
||||
},
|
||||
NoFreeSlot,
|
||||
InvalidProcess(ProcessId),
|
||||
WouldBlock,
|
||||
@@ -23,4 +26,19 @@ pub enum KernelError {
|
||||
EndpointFull,
|
||||
ProcessCreateDenied,
|
||||
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,
|
||||
}
|
||||
|
||||
86
crates/domain/src/generations.rs
Normal file
86
crates/domain/src/generations.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
36
crates/domain/src/generations_tests.rs
Normal file
36
crates/domain/src/generations_tests.rs
Normal 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");
|
||||
}
|
||||
@@ -1,38 +1,66 @@
|
||||
use types::id::{IrqId, ThreadId};
|
||||
|
||||
use crate::error::KernelError;
|
||||
use types::{
|
||||
badge::Badge,
|
||||
generation::Generation,
|
||||
id::{IrqId, NotificationId},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
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 {
|
||||
owners: [Option<ThreadId>; MAX_IRQS],
|
||||
routes: [Option<IrqRoute>; MAX_IRQS],
|
||||
}
|
||||
|
||||
impl IrqRoutingTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
owners: [None; MAX_IRQS],
|
||||
routes: [None; MAX_IRQS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> {
|
||||
let idx = irq.raw() as usize;
|
||||
if self.owners[idx].is_some() {
|
||||
return Err(KernelError::IrqAlreadyOwned(irq));
|
||||
}
|
||||
self.owners[idx] = Some(thread);
|
||||
Ok(())
|
||||
/// Whether `irq` is a line this table can route.
|
||||
pub fn is_valid(&self, irq: IrqId) -> bool {
|
||||
(irq.raw() as usize) < MAX_IRQS
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, irq: IrqId) {
|
||||
self.owners[irq.raw() as usize] = None;
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner(&self, irq: IrqId) -> Option<ThreadId> {
|
||||
self.owners[irq.raw() as usize]
|
||||
pub fn clear(&mut self, irq: IrqId) {
|
||||
if let Some(slot) = self.routes.get_mut(irq.raw() as usize) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The route for `irq`; `None` for unknown or unrouted lines.
|
||||
pub fn route(&self, irq: IrqId) -> Option<IrqRoute> {
|
||||
*self.routes.get(irq.raw() as usize)?
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IrqRoutingTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,43 @@
|
||||
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 {
|
||||
IrqId::from_raw(id)
|
||||
fn route(n: u16) -> IrqRoute {
|
||||
IrqRoute {
|
||||
notification: NotificationId::from_raw(n),
|
||||
generation: Generation::initial(),
|
||||
badge: Badge::new(1 << n),
|
||||
}
|
||||
|
||||
fn thread(id: u16) -> ThreadId {
|
||||
ThreadId::from_raw(id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_table_has_no_owners() {
|
||||
let table = IrqRoutingTable::new();
|
||||
assert!(table.owner(irq(0)).is_none());
|
||||
assert!(table.owner(irq(10)).is_none());
|
||||
fn new_table_routes_nothing() {
|
||||
let t = IrqRoutingTable::new();
|
||||
assert_eq!(t.route(IrqId::from_raw(10)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_and_lookup() {
|
||||
let mut table = IrqRoutingTable::new();
|
||||
table.register(irq(5), thread(1)).unwrap();
|
||||
assert_eq!(table.owner(irq(5)), Some(thread(1)));
|
||||
fn set_then_route() {
|
||||
let mut t = IrqRoutingTable::new();
|
||||
assert!(t.set(IrqId::from_raw(10), route(3)));
|
||||
assert_eq!(t.route(IrqId::from_raw(10)), Some(route(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_already_owned_fails() {
|
||||
let mut table = IrqRoutingTable::new();
|
||||
table.register(irq(5), thread(1)).unwrap();
|
||||
assert!(table.register(irq(5), thread(2)).is_err());
|
||||
fn set_replaces_and_clear_removes() {
|
||||
let mut t = IrqRoutingTable::new();
|
||||
t.set(IrqId::from_raw(10), route(3));
|
||||
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]
|
||||
fn unregister_then_reregister() {
|
||||
let mut table = IrqRoutingTable::new();
|
||||
table.register(irq(5), thread(1)).unwrap();
|
||||
table.unregister(irq(5));
|
||||
assert!(table.owner(irq(5)).is_none());
|
||||
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)));
|
||||
fn lines_beyond_the_table_are_rejected_without_panicking() {
|
||||
let mut t = IrqRoutingTable::new();
|
||||
assert!(!t.set(IrqId::from_raw(1000), route(1)));
|
||||
assert_eq!(t.route(IrqId::from_raw(1000)), None);
|
||||
t.clear(IrqId::from_raw(1000));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
#![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)]
|
||||
pub mod proofs;
|
||||
@@ -7,9 +21,11 @@ pub mod capability;
|
||||
pub mod endpoint;
|
||||
pub mod error;
|
||||
pub mod fault;
|
||||
pub mod generations;
|
||||
pub mod irq;
|
||||
pub mod memory;
|
||||
pub mod message;
|
||||
pub mod notification;
|
||||
pub mod process;
|
||||
pub mod resource;
|
||||
pub mod scheduler;
|
||||
|
||||
232
crates/domain/src/memory/frame/mod.rs
Normal file
232
crates/domain/src/memory/frame/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
195
crates/domain/src/memory/frame/proofs.rs
Normal file
195
crates/domain/src/memory/frame/proofs.rs
Normal 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));
|
||||
}
|
||||
188
crates/domain/src/memory/frame/tests.rs
Normal file
188
crates/domain/src/memory/frame/tests.rs
Normal 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]);
|
||||
}
|
||||
@@ -1,118 +1,210 @@
|
||||
use crate::{capability::CapRef, generations::SlotGenerations};
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
generation::Generation,
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
pub mod page_pool;
|
||||
pub mod process_memory;
|
||||
pub mod frame;
|
||||
pub mod quota;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "quota_tests.rs"]
|
||||
mod quota_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
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 {
|
||||
id: RegionId,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
generation: Generation,
|
||||
/// Where a region's frames come from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegionBacking {
|
||||
/// Frames from the user pool, freed with the region.
|
||||
Pool,
|
||||
/// Device MMIO; never freed.
|
||||
Device,
|
||||
}
|
||||
|
||||
impl MemoryRegion {
|
||||
pub fn new(
|
||||
id: RegionId,
|
||||
#[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,
|
||||
size: PageCount,
|
||||
generation: Generation,
|
||||
pages: PageCount,
|
||||
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
|
||||
impl Region {
|
||||
pub fn new(
|
||||
owner: ProcessId,
|
||||
backing: RegionBacking,
|
||||
base: PhysicalAddress,
|
||||
pages: PageCount,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
owner,
|
||||
backing,
|
||||
base,
|
||||
size,
|
||||
generation,
|
||||
pages,
|
||||
mappings: [None; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> RegionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> ProcessId {
|
||||
self.owner
|
||||
}
|
||||
|
||||
pub fn backing(&self) -> RegionBacking {
|
||||
self.backing
|
||||
}
|
||||
|
||||
pub fn base(&self) -> PhysicalAddress {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PageCount {
|
||||
self.size
|
||||
pub fn pages(&self) -> PageCount {
|
||||
self.pages
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> &Generation {
|
||||
&self.generation
|
||||
pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
|
||||
self.mappings.iter().flatten()
|
||||
}
|
||||
|
||||
pub fn revoke(&mut self) {
|
||||
self.generation = self.generation.next();
|
||||
/// Records that `mapping.process` maps this region. Each process maps a
|
||||
/// 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 {
|
||||
regions: [Option<MemoryRegion>; MAX_REGIONS],
|
||||
regions: [Option<Region>; MAX_REGIONS],
|
||||
generations: SlotGenerations<MAX_REGIONS>,
|
||||
}
|
||||
|
||||
impl RegionTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
regions: [const { None }; MAX_REGIONS],
|
||||
generations: SlotGenerations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, region: MemoryRegion) -> Option<RegionId> {
|
||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
||||
let id = RegionId::from_raw(idx as u16);
|
||||
self.regions[idx] = Some(region);
|
||||
Some(id)
|
||||
/// Generation capabilities to a live region must carry.
|
||||
pub fn generation(&self, id: RegionId) -> Option<Generation> {
|
||||
self.get(id)?;
|
||||
self.generations.current(id.raw() as usize)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: RegionId) -> Option<&MemoryRegion> {
|
||||
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> {
|
||||
fn free_slot(&self) -> Option<usize> {
|
||||
self.regions
|
||||
.iter()
|
||||
.filter_map(|s| s.as_ref())
|
||||
.filter(move |r| r.owner() == process)
|
||||
.enumerate()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
57
crates/domain/src/memory/quota.rs
Normal file
57
crates/domain/src/memory/quota.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
37
crates/domain/src/memory/quota_tests.rs
Normal file
37
crates/domain/src/memory/quota_tests.rs
Normal 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));
|
||||
}
|
||||
@@ -1,207 +1,118 @@
|
||||
use super::page_pool::PagePool;
|
||||
use super::process_memory::ProcessMemory;
|
||||
use super::*;
|
||||
use types::address::PhysicalAddress;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::bounded::PageCount;
|
||||
use types::generation::Generation;
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
fn base() -> PhysicalAddress {
|
||||
PhysicalAddress::from_raw(0x8000_0000)
|
||||
fn pid(n: u16) -> ProcessId {
|
||||
ProcessId::from_raw(n)
|
||||
}
|
||||
|
||||
fn process(id: u16) -> ProcessId {
|
||||
ProcessId::from_raw(id)
|
||||
fn va(addr: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(addr)
|
||||
}
|
||||
|
||||
fn pages(n: u16) -> PageCount {
|
||||
PageCount::new(n).unwrap()
|
||||
fn pool_region(owner: u16, pages: u16) -> Region {
|
||||
Region::new(
|
||||
pid(owner),
|
||||
RegionBacking::Pool,
|
||||
PhysicalAddress::from_raw(0x8400_0000),
|
||||
PageCount::new(pages).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
// --- PagePool tests ---
|
||||
|
||||
#[test]
|
||||
fn new_pool_has_all_pages_free() {
|
||||
let pool = PagePool::new(base(), 100);
|
||||
assert_eq!(pool.free_count(), 100);
|
||||
fn mapping(process: u16, addr: usize) -> RegionMapping {
|
||||
RegionMapping {
|
||||
process: pid(process),
|
||||
va: va(addr),
|
||||
perms: PagePerms::ReadWrite,
|
||||
via: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_reduces_free_count() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
pool.allocate(10).unwrap();
|
||||
assert_eq!(pool.free_count(), 90);
|
||||
fn new_region_has_no_mappings() {
|
||||
let r = pool_region(0, 4);
|
||||
assert_eq!(r.mappings().count(), 0);
|
||||
assert_eq!(r.owner(), pid(0));
|
||||
assert_eq!(r.backing(), RegionBacking::Pool);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_returns_base_aligned_address() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let addr = pool.allocate(1).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
fn a_process_maps_a_region_at_most_once() {
|
||||
let mut r = pool_region(0, 4);
|
||||
assert!(r.add_mapping(mapping(1, 0x30000)));
|
||||
assert!(!r.add_mapping(mapping(1, 0x50000)));
|
||||
assert!(r.add_mapping(mapping(2, 0x30000)));
|
||||
assert_eq!(r.mappings().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_sequential_returns_contiguous() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let a = pool.allocate(4).unwrap();
|
||||
let b = pool.allocate(4).unwrap();
|
||||
assert_eq!(a, base());
|
||||
assert_eq!(b, base().offset(4));
|
||||
fn mapping_slots_are_bounded() {
|
||||
let mut r = pool_region(0, 1);
|
||||
for p in 0..MAX_REGION_MAPPINGS as u16 {
|
||||
assert!(r.add_mapping(mapping(p, 0x30000)));
|
||||
}
|
||||
assert!(!r.add_mapping(mapping(99, 0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_too_many_returns_none() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(pool.allocate(11).is_none());
|
||||
fn remove_mapping_returns_it() {
|
||||
let mut r = pool_region(0, 4);
|
||||
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]
|
||||
fn free_then_reallocate() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(pool.free_count(), 5);
|
||||
assert!(pool.free(addr, 5));
|
||||
assert_eq!(pool.free_count(), 10);
|
||||
let addr2 = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr2, base());
|
||||
fn covers_checks_every_page_of_a_mapping() {
|
||||
let mut r = pool_region(0, 4);
|
||||
r.add_mapping(mapping(1, 0x30000));
|
||||
assert!(r.covers(pid(1), va(0x30000)));
|
||||
assert!(r.covers(pid(1), va(0x33000)));
|
||||
assert!(!r.covers(pid(1), va(0x34000)));
|
||||
assert!(!r.covers(pid(1), va(0x2F000)));
|
||||
assert!(!r.covers(pid(2), va(0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_out_of_range_returns_false() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(!pool.free(PhysicalAddress::from_raw(0), 1));
|
||||
fn region_table_insert_get_remove() {
|
||||
let mut t = RegionTable::new();
|
||||
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]
|
||||
fn allocate_fragmented_finds_gap() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let a = pool.allocate(3).unwrap();
|
||||
let _b = pool.allocate(3).unwrap();
|
||||
let _c = pool.allocate(3).unwrap();
|
||||
pool.free(a, 3);
|
||||
// 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());
|
||||
fn region_table_is_bounded() {
|
||||
let mut t = RegionTable::new();
|
||||
for _ in 0..MAX_REGIONS {
|
||||
assert!(t.insert(pool_region(0, 1)).is_some());
|
||||
}
|
||||
assert!(t.insert(pool_region(0, 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_exact_remaining() {
|
||||
let mut pool = PagePool::new(base(), 5);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
assert_eq!(pool.free_count(), 0);
|
||||
assert!(pool.allocate(1).is_none());
|
||||
fn region_table_finds_regions_owned_by_a_process() {
|
||||
let mut t = RegionTable::new();
|
||||
let a = t.insert(pool_region(1, 1)).unwrap();
|
||||
t.insert(pool_region(2, 1)).unwrap();
|
||||
let c = t.insert(pool_region(1, 1)).unwrap();
|
||||
let mut ids = [RegionId::from_raw(u16::MAX); 4];
|
||||
let mut n = 0;
|
||||
for id in t.ids_owned_by(pid(1)) {
|
||||
ids[n] = id;
|
||||
n += 1;
|
||||
}
|
||||
|
||||
// --- ProcessMemory tests ---
|
||||
|
||||
#[test]
|
||||
fn new_process_memory_has_zero_pages() {
|
||||
let pm = ProcessMemory::new(process(0), 100);
|
||||
assert_eq!(pm.page_count(), 0);
|
||||
assert_eq!(pm.limit(), 100);
|
||||
assert_eq!(&ids[..n], &[a, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_pages_within_limit() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
assert!(pm.grant(10).is_ok());
|
||||
assert_eq!(pm.page_count(), 10);
|
||||
}
|
||||
|
||||
#[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);
|
||||
fn region_table_finds_mapping_covering_a_va() {
|
||||
let mut t = RegionTable::new();
|
||||
let id = t.insert(pool_region(1, 2)).unwrap();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -5,72 +5,38 @@ use types::bounded::CapSlot;
|
||||
mod tests;
|
||||
|
||||
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 {
|
||||
words: [usize; MAX_MESSAGE_WORDS],
|
||||
caps: [Option<CapSlot>; MAX_CAP_TRANSFERS],
|
||||
cap: Option<CapSlot>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
words: [0; MAX_MESSAGE_WORDS],
|
||||
caps: [None; MAX_CAP_TRANSFERS],
|
||||
}
|
||||
Self::from_words([0; MAX_MESSAGE_WORDS])
|
||||
}
|
||||
|
||||
pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self {
|
||||
Self {
|
||||
words,
|
||||
caps: [None; MAX_CAP_TRANSFERS],
|
||||
Self { words, cap: None }
|
||||
}
|
||||
|
||||
pub fn with_cap(mut self, slot: CapSlot) -> Self {
|
||||
self.cap = Some(slot);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn words(&self) -> &[usize; MAX_MESSAGE_WORDS] {
|
||||
&self.words
|
||||
}
|
||||
|
||||
/// Word `index`, or 0 past the end.
|
||||
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] {
|
||||
&self.caps
|
||||
}
|
||||
|
||||
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
|
||||
pub fn cap(&self) -> Option<CapSlot> {
|
||||
self.cap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,23 @@
|
||||
use super::*;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
fn slot(n: u8) -> CapSlot {
|
||||
CapSlot::new(n).unwrap()
|
||||
}
|
||||
|
||||
// --- Construction ---
|
||||
|
||||
#[test]
|
||||
fn empty_message_has_zeroed_words_and_no_caps() {
|
||||
fn empty_message_has_zeroed_words_and_no_cap() {
|
||||
let msg = Message::empty();
|
||||
assert_eq!(msg.words(), &[0; MAX_MESSAGE_WORDS]);
|
||||
for i in 0..MAX_CAP_TRANSFERS {
|
||||
assert!(msg.cap(i).is_none());
|
||||
}
|
||||
assert!(msg.cap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_words_sets_words_and_no_caps() {
|
||||
fn from_words_keeps_every_word() {
|
||||
let msg = Message::from_words([10, 20, 30, 40]);
|
||||
assert_eq!(msg.word(0), 10);
|
||||
assert_eq!(msg.word(1), 20);
|
||||
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]);
|
||||
assert_eq!(msg.words(), &[10, 20, 30, 40]);
|
||||
assert!(msg.cap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_returns_full_array() {
|
||||
let msg = Message::empty();
|
||||
assert_eq!(msg.caps(), &[None; MAX_CAP_TRANSFERS]);
|
||||
}
|
||||
|
||||
// --- 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]);
|
||||
fn with_cap_attaches_one_capability() {
|
||||
let msg = Message::from_words([1, 2, 3, 4]).with_cap(CapSlot::new(5).unwrap());
|
||||
assert_eq!(msg.cap(), CapSlot::new(5));
|
||||
assert_eq!(msg.word(3), 4);
|
||||
}
|
||||
|
||||
192
crates/domain/src/notification/mod.rs
Normal file
192
crates/domain/src/notification/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
43
crates/domain/src/notification/proofs.rs
Normal file
43
crates/domain/src/notification/proofs.rs
Normal 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);
|
||||
}
|
||||
116
crates/domain/src/notification/tests.rs
Normal file
116
crates/domain/src/notification/tests.rs
Normal 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);
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
use types::{
|
||||
bounded::CapSlot,
|
||||
generation::Generation,
|
||||
id::{ProcessId, ThreadId},
|
||||
};
|
||||
|
||||
use crate::{capability::CapabilityTable, error::KernelError};
|
||||
use crate::{capability::CapabilityTable, error::KernelError, generations::SlotGenerations};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
pub const MAX_THREADS_PER_PROCESS: usize = 4;
|
||||
pub const MAX_PROCESSES: usize = 64;
|
||||
pub const MAX_THREADS_PER_PROCESS: usize = if cfg!(kani) { 2 } else { 4 };
|
||||
pub const MAX_PROCESSES: usize = if cfg!(kani) { 3 } else { 64 };
|
||||
|
||||
pub struct Process {
|
||||
id: ProcessId,
|
||||
@@ -75,28 +76,38 @@ impl Process {
|
||||
|
||||
pub struct ProcessTable {
|
||||
processes: [Option<Process>; MAX_PROCESSES],
|
||||
generations: SlotGenerations<MAX_PROCESSES>,
|
||||
}
|
||||
|
||||
impl ProcessTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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(
|
||||
&mut self,
|
||||
capabilities: CapabilityTable,
|
||||
fault_handler: Option<CapSlot>,
|
||||
) -> Result<ProcessId, KernelError> {
|
||||
let idx = self
|
||||
let generations = &self.generations;
|
||||
let (idx, slot) = self
|
||||
.processes
|
||||
.iter()
|
||||
.position(|s| s.is_none())
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find(|(i, s)| s.is_none() && generations.is_usable(*i))
|
||||
.ok_or(KernelError::TooManyProcesses)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -116,11 +127,22 @@ impl ProcessTable {
|
||||
.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> {
|
||||
let idx = id.raw() as usize;
|
||||
self.processes
|
||||
let process = self
|
||||
.processes
|
||||
.get_mut(idx)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use crate::capability::CapabilityTable;
|
||||
use crate::resource::Resource;
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use types::permission::PermissionSet;
|
||||
|
||||
#[kani::proof]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use crate::capability::{Capability, CapabilityTable};
|
||||
use crate::resource::Resource;
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use types::generation::Generation;
|
||||
use types::id::EndpointId;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
@@ -16,7 +16,10 @@ fn delegation_via_cap_table_never_escalates() {
|
||||
kani::assume(source_perms.allows(Permission::Delegate));
|
||||
|
||||
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 original_perms = *cap.permissions();
|
||||
@@ -43,11 +46,18 @@ fn delegation_cannot_add_permissions_not_in_source() {
|
||||
let source = PermissionSet::from_bits(source_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::Write) { 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)); }
|
||||
if !source.allows(Permission::Revoke) { assert!(!result.allows(Permission::Revoke)); }
|
||||
if !source.allows(Permission::Read) {
|
||||
assert!(!result.allows(Permission::Read));
|
||||
}
|
||||
if !source.allows(Permission::Write) {
|
||||
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]
|
||||
|
||||
@@ -22,7 +22,7 @@ fn dead_thread_rejects_all_transitions() {
|
||||
|
||||
assert!(matches!(thread.state(), ThreadState::Dead));
|
||||
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.preempt().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());
|
||||
|
||||
assert!(matches!(thread.state(), ThreadState::Ready));
|
||||
assert!(thread.block(BlockReason::WaitingAny).is_err());
|
||||
assert!(thread.block(BlockReason::AwaitingReply).is_err());
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
@@ -44,7 +44,7 @@ fn blocked_thread_cannot_run_directly() {
|
||||
let mut thread = test_thread(Priority::new(16).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());
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
Memory(RegionId),
|
||||
Endpoint(EndpointId),
|
||||
Notification(NotificationId),
|
||||
Process(ProcessId),
|
||||
Interrupt(IrqId),
|
||||
ProcessCreate,
|
||||
/// Machine-wide controls such as halting; given only to init at boot.
|
||||
SystemControl,
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use types::{
|
||||
id::{CoreId, ThreadId},
|
||||
};
|
||||
|
||||
use super::{per_core::PerCoreScheduler, Scheduler, SchedulerDecision};
|
||||
use super::{Scheduler, SchedulerDecision, per_core::PerCoreScheduler};
|
||||
|
||||
const MAX_CORES: usize = 8;
|
||||
const MAX_SCHEDULED_THREADS: usize = 256;
|
||||
const MAX_CORES: usize = if cfg!(kani) { 1 } else { 8 };
|
||||
const MAX_SCHEDULED_THREADS: usize = crate::thread::MAX_THREADS;
|
||||
const BALANCE_THRESHOLD: usize = 2;
|
||||
|
||||
struct ThreadPlacement {
|
||||
@@ -25,17 +25,64 @@ impl MultiCoreScheduler {
|
||||
let cores = core::array::from_fn(|i| PerCoreScheduler::new(CoreId::from_raw(i as u8)));
|
||||
Self {
|
||||
cores,
|
||||
num_cores,
|
||||
num_cores: num_cores.min(MAX_CORES as u8),
|
||||
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> {
|
||||
self.cores[core.raw() as usize].current()
|
||||
self.core(core)?.current()
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -47,53 +94,63 @@ impl MultiCoreScheduler {
|
||||
|
||||
fn least_loaded_core(&self) -> Option<CoreId> {
|
||||
(0..self.num_cores)
|
||||
.map(|i| CoreId::from_raw(i))
|
||||
.map(CoreId::from_raw)
|
||||
.min_by_key(|&c| self.thread_count_on(c))
|
||||
}
|
||||
|
||||
fn most_loaded_core(&self) -> Option<CoreId> {
|
||||
(0..self.num_cores)
|
||||
.map(|i| CoreId::from_raw(i))
|
||||
.map(CoreId::from_raw)
|
||||
.max_by_key(|&c| self.thread_count_on(c))
|
||||
}
|
||||
}
|
||||
|
||||
impl Scheduler for MultiCoreScheduler {
|
||||
fn add(&mut self, thread: ThreadId, priority: Priority, core: CoreId) {
|
||||
let idx = thread.raw() as usize;
|
||||
self.placements[idx] = Some(ThreadPlacement { core, priority });
|
||||
self.cores[core.raw() as usize].add(thread, priority);
|
||||
if self.core(core).is_none() {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
let idx = thread.raw() as usize;
|
||||
if let Some(placement) = self.placements[idx].take() {
|
||||
let core_idx = placement.core.raw() as usize;
|
||||
self.cores[core_idx].remove(thread, placement.priority);
|
||||
if self.cores[core_idx].current() == Some(thread) {
|
||||
self.cores[core_idx].clear_current();
|
||||
let Some(placement) = self
|
||||
.placements
|
||||
.get_mut(thread.raw() as usize)
|
||||
.and_then(Option::take)
|
||||
else {
|
||||
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> {
|
||||
self.cores[core.raw() as usize].pick_next()
|
||||
self.core_mut(core)?.pick_next()
|
||||
}
|
||||
|
||||
fn tick(&mut self, core: CoreId, current: ThreadId) -> SchedulerDecision {
|
||||
let idx = current.raw() as usize;
|
||||
if let Some(placement) = &self.placements[idx] {
|
||||
let current_priority = placement.priority;
|
||||
let core_idx = core.raw() as usize;
|
||||
if let Some(highest) = self.cores[core_idx].highest_waiting_priority() {
|
||||
if highest > current_priority {
|
||||
if let Some(next) = self.cores[core_idx].pick_next() {
|
||||
self.cores[core_idx].add(current, current_priority);
|
||||
let Some(current_priority) = self.placement(current).map(|p| p.priority) else {
|
||||
return SchedulerDecision::Continue;
|
||||
};
|
||||
if let Some(c) = self.core_mut(core)
|
||||
&& let Some(highest) = c.highest_waiting_priority()
|
||||
&& highest > current_priority
|
||||
&& let Some(next) = c.pick_next()
|
||||
{
|
||||
c.add(current, current_priority);
|
||||
return SchedulerDecision::Preempt(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SchedulerDecision::Continue
|
||||
}
|
||||
|
||||
@@ -103,13 +160,14 @@ impl Scheduler for MultiCoreScheduler {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if self.thread_count_on(busiest) > self.thread_count_on(lightest) + BALANCE_THRESHOLD {
|
||||
let busiest_idx = busiest.raw() as usize;
|
||||
if let Some((thread, priority)) = self.cores[busiest_idx].steal_lowest() {
|
||||
let lightest_idx = lightest.raw() as usize;
|
||||
self.cores[lightest_idx].add(thread, priority);
|
||||
let tid = thread.raw() as usize;
|
||||
self.placements[tid] = Some(ThreadPlacement {
|
||||
if self.thread_count_on(busiest) > self.thread_count_on(lightest) + BALANCE_THRESHOLD
|
||||
&& let Some((thread, priority)) = self.core_mut(busiest).and_then(|c| c.steal_lowest())
|
||||
{
|
||||
if let Some(c) = self.core_mut(lightest) {
|
||||
c.add(thread, priority);
|
||||
}
|
||||
if let Some(p) = self.placements.get_mut(thread.raw() as usize) {
|
||||
*p = Some(ThreadPlacement {
|
||||
core: lightest,
|
||||
priority,
|
||||
});
|
||||
|
||||
@@ -29,16 +29,29 @@ impl PerCoreScheduler {
|
||||
self.queue.add(thread, priority);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, thread: ThreadId, priority: Priority) {
|
||||
self.queue.remove(thread, priority);
|
||||
pub fn is_queued(&self, thread: ThreadId) -> bool {
|
||||
self.queue.contains(thread)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, thread: ThreadId) {
|
||||
self.queue.remove(thread);
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
self.current = None;
|
||||
}
|
||||
@@ -56,6 +69,10 @@ impl PerCoreScheduler {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,71 +2,42 @@ use types::bounded::Priority;
|
||||
use types::id::ThreadId;
|
||||
|
||||
const NUM_PRIORITIES: usize = 32;
|
||||
const MAX_THREADS_PER_PRIORITY: usize = 16;
|
||||
const MAX_THREADS: usize = crate::thread::MAX_THREADS;
|
||||
|
||||
struct RunList {
|
||||
threads: [Option<ThreadId>; MAX_THREADS_PER_PRIORITY],
|
||||
head: usize,
|
||||
tail: usize,
|
||||
count: u8,
|
||||
#[derive(Clone, Copy)]
|
||||
struct Links {
|
||||
prev: Option<ThreadId>,
|
||||
next: Option<ThreadId>,
|
||||
queued_at: Option<u8>,
|
||||
}
|
||||
|
||||
impl RunList {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
threads: [None; MAX_THREADS_PER_PRIORITY],
|
||||
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
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct ListEnds {
|
||||
head: Option<ThreadId>,
|
||||
tail: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
queues: [RunList; NUM_PRIORITIES],
|
||||
lists: [ListEnds; NUM_PRIORITIES],
|
||||
links: [Links; MAX_THREADS],
|
||||
bitmap: u32,
|
||||
}
|
||||
|
||||
impl PriorityQueue {
|
||||
pub fn new() -> 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,
|
||||
}
|
||||
}
|
||||
@@ -75,34 +46,107 @@ impl PriorityQueue {
|
||||
self.bitmap
|
||||
}
|
||||
|
||||
pub fn add(&mut self, thread: ThreadId, priority: Priority) {
|
||||
let idx = priority.value() as usize;
|
||||
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 len(&self) -> usize {
|
||||
self.links.iter().filter(|l| l.queued_at.is_some()).count()
|
||||
}
|
||||
|
||||
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;
|
||||
self.queues[idx].remove(thread);
|
||||
if self.queues[idx].is_empty() {
|
||||
let queued = self.link(thread).is_none_or(|l| l.queued_at.is_some());
|
||||
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);
|
||||
}
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
// highest set bit = highest priority
|
||||
let idx = 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)?))
|
||||
self.pop_level(31 - self.bitmap.leading_zeros() as usize)
|
||||
}
|
||||
|
||||
pub fn highest_priority(&self) -> Option<Priority> {
|
||||
@@ -117,15 +161,16 @@ impl PriorityQueue {
|
||||
if self.bitmap == 0 {
|
||||
return None;
|
||||
}
|
||||
let idx = 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)?))
|
||||
self.pop_level(self.bitmap.trailing_zeros() as usize)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bitmap == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PriorityQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ fn prio(value: u8) -> Priority {
|
||||
#[test]
|
||||
fn empty_queue_returns_none() {
|
||||
let mut q = PriorityQueue::new();
|
||||
assert!(q.next().is_none());
|
||||
assert!(q.pop_highest().is_none());
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ fn single_thread() {
|
||||
let mut q = PriorityQueue::new();
|
||||
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!(p, prio(10));
|
||||
}
|
||||
@@ -43,7 +43,7 @@ fn highest_priority_runs_first() {
|
||||
q.add(thread(2), prio(20));
|
||||
q.add(thread(3), prio(10));
|
||||
|
||||
let (t, _) = q.next().unwrap();
|
||||
let (t, _) = q.pop_highest().unwrap();
|
||||
assert_eq!(t, thread(2));
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ fn same_priority_is_fifo() {
|
||||
q.add(thread(2), prio(10));
|
||||
q.add(thread(3), prio(10));
|
||||
|
||||
assert_eq!(q.next().unwrap().0, thread(1));
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.next().unwrap().0, thread(3));
|
||||
assert!(q.next().is_none());
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
||||
assert!(q.pop_highest().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -66,18 +66,18 @@ fn remove_thread() {
|
||||
q.add(thread(1), 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!(q.next().is_none());
|
||||
assert!(q.pop_highest().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_clears_bitmap_when_empty() {
|
||||
let mut q = PriorityQueue::new();
|
||||
q.add(thread(1), prio(10));
|
||||
q.remove(thread(1), prio(10));
|
||||
q.remove(thread(1));
|
||||
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
@@ -89,9 +89,9 @@ fn drain_all_priorities() {
|
||||
q.add(thread(2), prio(15));
|
||||
q.add(thread(3), prio(31));
|
||||
|
||||
assert_eq!(q.next().unwrap().0, thread(3)); // highest first
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.next().unwrap().0, thread(1)); // lowest last
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3)); // highest first
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1)); // lowest last
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ fn highest_priority_peeks_without_popping() {
|
||||
q.add(thread(1), prio(5));
|
||||
q.add(thread(2), 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]
|
||||
@@ -154,7 +154,7 @@ fn steal_lowest_takes_lowest_priority_thread() {
|
||||
let (t, p) = q.steal_lowest().unwrap();
|
||||
assert_eq!(t, thread(1));
|
||||
assert_eq!(p, prio(5));
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
}
|
||||
|
||||
#[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(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));
|
||||
}
|
||||
|
||||
@@ -1,43 +1,13 @@
|
||||
use types::{
|
||||
address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
|
||||
badge::Badge,
|
||||
bounded::{CapSlot, PageCount},
|
||||
mapping::PagePerms,
|
||||
permission::PermissionSet,
|
||||
};
|
||||
|
||||
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 {
|
||||
Send {
|
||||
endpoint: CapSlot,
|
||||
@@ -51,15 +21,47 @@ pub enum Syscall {
|
||||
to_endpoint: CapSlot,
|
||||
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,
|
||||
notification: CapSlot,
|
||||
},
|
||||
AcknowledgeInterrupt {
|
||||
irq: CapSlot,
|
||||
},
|
||||
WaitAny {
|
||||
sources: WaitSet,
|
||||
},
|
||||
CreateProcess {
|
||||
create_cap: CapSlot,
|
||||
image: DataRef,
|
||||
@@ -67,9 +69,30 @@ pub enum Syscall {
|
||||
Kill {
|
||||
process: CapSlot,
|
||||
},
|
||||
/// Deletes every capability derived from `slot`, keeping `slot`.
|
||||
Revoke {
|
||||
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 {
|
||||
entry: EntryPoint,
|
||||
stack: StackPointer,
|
||||
@@ -86,9 +109,14 @@ pub enum Syscall {
|
||||
GetProcessId,
|
||||
Yield,
|
||||
Exit,
|
||||
Halt,
|
||||
/// Stops the machine; needs a SystemControl capability.
|
||||
Halt {
|
||||
control: CapSlot,
|
||||
},
|
||||
CreateEndpoint,
|
||||
DebugPutchar { byte: u8 },
|
||||
DebugPutchar {
|
||||
byte: u8,
|
||||
},
|
||||
MapPage {
|
||||
process: CapSlot,
|
||||
src_va: VirtualAddress,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use types::{
|
||||
bounded::{CapSlot, Priority},
|
||||
id::{CoreId, ProcessId, ThreadId},
|
||||
bounded::Priority,
|
||||
generation::Generation,
|
||||
id::{CoreId, EndpointId, NotificationId, ProcessId, ThreadId},
|
||||
time::TimeSlice,
|
||||
};
|
||||
|
||||
use crate::error::KernelError;
|
||||
use crate::{error::KernelError, generations::SlotGenerations};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
@@ -20,6 +21,7 @@ pub struct Thread {
|
||||
core_affinity: Option<CoreId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThreadState {
|
||||
Ready,
|
||||
Running(CoreId),
|
||||
@@ -77,11 +79,18 @@ impl SavedContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BlockReason {
|
||||
Sending(CapSlot),
|
||||
Receiving(CapSlot),
|
||||
WaitingForInterrupt(CapSlot),
|
||||
WaitingAny,
|
||||
/// Queued as a sender on the endpoint.
|
||||
Sending(EndpointId),
|
||||
/// Queued as a `Call`er on the endpoint; will then await the reply.
|
||||
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 {
|
||||
@@ -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> {
|
||||
match self.state {
|
||||
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 {
|
||||
threads: [Option<Thread>; MAX_THREADS],
|
||||
generations: SlotGenerations<MAX_THREADS>,
|
||||
}
|
||||
|
||||
impl ThreadTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
@@ -229,14 +265,19 @@ impl ThreadTable {
|
||||
time_slice: TimeSlice,
|
||||
core_affinity: Option<CoreId>,
|
||||
) -> Result<ThreadId, KernelError> {
|
||||
let idx = self
|
||||
.threads
|
||||
.iter()
|
||||
.position(|s| s.is_none())
|
||||
.ok_or(KernelError::TooManyThreads)?;
|
||||
let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -248,17 +289,31 @@ impl ThreadTable {
|
||||
core_affinity: Option<CoreId>,
|
||||
context: SavedContext,
|
||||
) -> Result<ThreadId, KernelError> {
|
||||
let idx = self
|
||||
.threads
|
||||
.iter()
|
||||
.position(|s| s.is_none())
|
||||
.ok_or(KernelError::TooManyThreads)?;
|
||||
let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let idx = id.raw() as usize;
|
||||
self.threads
|
||||
@@ -277,10 +332,13 @@ impl ThreadTable {
|
||||
|
||||
pub fn destroy(&mut self, id: ThreadId) -> Result<Thread, KernelError> {
|
||||
let idx = id.raw() as usize;
|
||||
self.threads
|
||||
let thread = self
|
||||
.threads
|
||||
.get_mut(idx)
|
||||
.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> {
|
||||
@@ -290,3 +348,9 @@ impl ThreadTable {
|
||||
.filter(move |t| t.process() == process)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThreadTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use types::bounded::Priority;
|
||||
use types::id::{CoreId, ProcessId, ThreadId};
|
||||
use types::id::{CoreId, EndpointId, ProcessId, ThreadId};
|
||||
use types::time::TimeSlice;
|
||||
|
||||
fn thread(id: u16) -> ThreadId {
|
||||
@@ -65,15 +65,18 @@ fn ready_to_running() {
|
||||
fn running_to_blocked() {
|
||||
let mut t = test_thread(0, 0);
|
||||
t.set_running(core(0)).unwrap();
|
||||
t.block(BlockReason::WaitingAny).unwrap();
|
||||
assert!(matches!(t.state(), ThreadState::Blocked(BlockReason::WaitingAny)));
|
||||
t.block(BlockReason::AwaitingReply).unwrap();
|
||||
assert!(matches!(
|
||||
t.state(),
|
||||
ThreadState::Blocked(BlockReason::AwaitingReply)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_to_ready() {
|
||||
let mut t = test_thread(0, 0);
|
||||
t.set_running(core(0)).unwrap();
|
||||
t.block(BlockReason::WaitingAny).unwrap();
|
||||
t.block(BlockReason::AwaitingReply).unwrap();
|
||||
t.unblock().unwrap();
|
||||
assert!(matches!(t.state(), ThreadState::Ready));
|
||||
}
|
||||
@@ -102,7 +105,7 @@ fn dead_cannot_transition() {
|
||||
t.set_running(core(0)).unwrap();
|
||||
t.exit().unwrap();
|
||||
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.preempt().is_err());
|
||||
assert!(t.exit().is_err());
|
||||
@@ -111,14 +114,14 @@ fn dead_cannot_transition() {
|
||||
#[test]
|
||||
fn ready_cannot_block() {
|
||||
let mut t = test_thread(0, 0);
|
||||
assert!(t.block(BlockReason::WaitingAny).is_err());
|
||||
assert!(t.block(BlockReason::AwaitingReply).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_cannot_run_directly() {
|
||||
let mut t = test_thread(0, 0);
|
||||
t.set_running(core(0)).unwrap();
|
||||
t.block(BlockReason::WaitingAny).unwrap();
|
||||
t.block(BlockReason::AwaitingReply).unwrap();
|
||||
assert!(t.set_running(core(0)).is_err());
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ fn ready_cannot_exit() {
|
||||
fn blocked_cannot_preempt() {
|
||||
let mut t = test_thread(0, 0);
|
||||
t.set_running(core(0)).unwrap();
|
||||
t.block(BlockReason::WaitingAny).unwrap();
|
||||
t.block(BlockReason::AwaitingReply).unwrap();
|
||||
assert!(t.preempt().is_err());
|
||||
}
|
||||
|
||||
@@ -162,7 +165,9 @@ fn time_slice_mut_ticks() {
|
||||
#[test]
|
||||
fn table_spawn_and_lookup() {
|
||||
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();
|
||||
assert_eq!(t.id(), id);
|
||||
assert_eq!(t.process(), process(0));
|
||||
@@ -171,15 +176,21 @@ fn table_spawn_and_lookup() {
|
||||
#[test]
|
||||
fn table_spawn_returns_unique_ids() {
|
||||
let mut table = ThreadTable::new();
|
||||
let a = table.spawn(process(0), prio(10), time_slice(), None).unwrap();
|
||||
let b = table.spawn(process(0), prio(10), time_slice(), None).unwrap();
|
||||
let a = table
|
||||
.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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_destroy_then_get_fails() {
|
||||
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();
|
||||
assert!(table.get(id).is_err());
|
||||
}
|
||||
@@ -187,9 +198,15 @@ fn table_destroy_then_get_fails() {
|
||||
#[test]
|
||||
fn table_threads_for_process_filters() {
|
||||
let mut table = ThreadTable::new();
|
||||
table.spawn(process(0), prio(10), time_slice(), None).unwrap();
|
||||
table.spawn(process(1), prio(10), time_slice(), None).unwrap();
|
||||
table.spawn(process(0), prio(5), time_slice(), None).unwrap();
|
||||
table
|
||||
.spawn(process(0), prio(10), time_slice(), None)
|
||||
.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();
|
||||
assert_eq!(count, 2);
|
||||
@@ -198,16 +215,54 @@ fn table_threads_for_process_filters() {
|
||||
#[test]
|
||||
fn table_get_mut_allows_state_transition() {
|
||||
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();
|
||||
assert!(matches!(table.get(id).unwrap().state(), ThreadState::Running(_)));
|
||||
assert!(matches!(
|
||||
table.get(id).unwrap().state(),
|
||||
ThreadState::Running(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_destroy_reuses_slot() {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ impl ElfInfo {
|
||||
pub fn total_pages(&self) -> usize {
|
||||
let mut total = 0;
|
||||
for seg in self.segments() {
|
||||
total += (seg.mem_size + 4095) / 4096;
|
||||
total += seg.mem_size.div_ceil(4096);
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
@@ -87,9 +87,11 @@ pub const MMIO_FIRST: usize = 0x10001000;
|
||||
pub const MMIO_STRIDE: usize = 0x1000;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,3 +7,6 @@ edition = "2024"
|
||||
types = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
platform = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
|
||||
|
||||
162
crates/kernel/src/abi.rs
Normal file
162
crates/kernel/src/abi.rs
Normal 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
|
||||
}
|
||||
79
crates/kernel/src/board.rs
Normal file
79
crates/kernel/src/board.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
172
crates/kernel/src/captree.rs
Normal file
172
crates/kernel/src/captree.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use domain::scheduler::{Scheduler, SchedulerDecision};
|
||||
use platform::Platform;
|
||||
use types::id::{CoreId, IrqId, ThreadId};
|
||||
use types::id::CoreId;
|
||||
|
||||
use crate::Kernel;
|
||||
|
||||
@@ -26,15 +26,26 @@ impl<P: Platform> Kernel<P> {
|
||||
self.scheduler.add(current_tid, priority, core);
|
||||
match self.scheduler.next(core) {
|
||||
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)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -42,22 +53,4 @@ impl<P: Platform> Kernel<P> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
#![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;
|
||||
mod memory;
|
||||
#[cfg(kani)]
|
||||
pub mod proofs;
|
||||
mod smp;
|
||||
pub mod syscall;
|
||||
|
||||
pub use board::BoardDevice;
|
||||
pub use syscall::{SyscallOutcome, SyscallResult};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -16,20 +36,16 @@ use domain::{
|
||||
endpoint::EndpointTable,
|
||||
error::KernelError,
|
||||
irq::IrqRoutingTable,
|
||||
memory::{
|
||||
page_pool::PagePool,
|
||||
process_memory::ProcessMemory,
|
||||
RegionTable,
|
||||
},
|
||||
message::Message,
|
||||
process::{ProcessTable, MAX_PROCESSES},
|
||||
memory::{RegionTable, frame::FrameTable, quota::Quota},
|
||||
notification::{MAX_NOTIFICATIONS, NotificationTable},
|
||||
process::{MAX_PROCESSES, ProcessTable},
|
||||
resource::Resource,
|
||||
scheduler::{multi_core::MultiCoreScheduler, Scheduler},
|
||||
syscall::WaitSet,
|
||||
thread::{ThreadTable, ThreadState, MAX_THREADS},
|
||||
scheduler::{Scheduler, multi_core::MultiCoreScheduler},
|
||||
thread::{MAX_THREADS, ThreadState, ThreadTable},
|
||||
};
|
||||
use platform::Platform;
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
bounded::{CapSlot, Priority},
|
||||
generation::Generation,
|
||||
id::{ProcessId, ThreadId},
|
||||
@@ -43,17 +59,19 @@ pub struct Kernel<P: Platform> {
|
||||
threads: ThreadTable,
|
||||
endpoints: EndpointTable,
|
||||
scheduler: MultiCoreScheduler,
|
||||
page_pool: PagePool,
|
||||
process_memory: [Option<ProcessMemory>; MAX_PROCESSES],
|
||||
frames: FrameTable,
|
||||
quotas: [Quota; MAX_PROCESSES],
|
||||
regions: RegionTable,
|
||||
irqs: IrqRoutingTable,
|
||||
pending_messages: [Option<Message>; MAX_THREADS],
|
||||
notifications: NotificationTable,
|
||||
ipc: [syscall::IpcState; 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> {
|
||||
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();
|
||||
Self {
|
||||
platform,
|
||||
@@ -61,13 +79,47 @@ impl<P: Platform> Kernel<P> {
|
||||
threads: ThreadTable::new(),
|
||||
endpoints: EndpointTable::new(),
|
||||
scheduler: MultiCoreScheduler::new(num_cores),
|
||||
page_pool,
|
||||
process_memory: [const { None }; MAX_PROCESSES],
|
||||
frames: FrameTable::new(pool_base, pool_pages),
|
||||
quotas: [Quota::default(); MAX_PROCESSES],
|
||||
regions: RegionTable::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],
|
||||
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,
|
||||
) -> Result<(ProcessId, ThreadId), KernelError> {
|
||||
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)?;
|
||||
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(
|
||||
Resource::ProcessCreate,
|
||||
PermissionSet::empty().with(Permission::Execute),
|
||||
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();
|
||||
self.set_home(tid, core);
|
||||
self.scheduler.add(tid, priority, core);
|
||||
self.scheduler.next(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> {
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn free_page_count(&self) -> usize {
|
||||
self.page_pool.free_count()
|
||||
}
|
||||
|
||||
pub fn process_exists(&self, pid: ProcessId) -> bool {
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn thread_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_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> {
|
||||
@@ -146,30 +209,31 @@ impl<P: Platform> Kernel<P> {
|
||||
|
||||
pub fn create_process_internal(&mut self) -> Option<ProcessId> {
|
||||
let pid = self.processes.create(CapabilityTable::new(), None).ok()?;
|
||||
self.process_memory[pid.raw() as usize] =
|
||||
Some(domain::memory::process_memory::ProcessMemory::new(pid, ProcessMemory::DEFAULT_PAGE_LIMIT));
|
||||
if self.create_address_space(pid).is_err() {
|
||||
let _ = self.processes.destroy(pid);
|
||||
return None;
|
||||
}
|
||||
Some(pid)
|
||||
}
|
||||
|
||||
pub fn spawn_thread_internal(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
) -> Option<ThreadId> {
|
||||
use domain::scheduler::Scheduler;
|
||||
let tid = self.threads.spawn(
|
||||
pid,
|
||||
Priority::DEFAULT,
|
||||
TimeSlice::DEFAULT,
|
||||
None,
|
||||
).ok()?;
|
||||
pub fn spawn_thread_internal(&mut self, pid: ProcessId) -> Option<ThreadId> {
|
||||
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 core = self.platform.core_id();
|
||||
self.scheduler.add(tid, Priority::DEFAULT, core);
|
||||
self.place(tid);
|
||||
self.make_ready(tid, Priority::DEFAULT);
|
||||
Some(tid)
|
||||
}
|
||||
|
||||
pub fn create_endpoint(&mut self) -> Result<types::id::EndpointId, KernelError> {
|
||||
self.endpoints.create().ok_or(KernelError::NoFreeSlot)
|
||||
/// Creates an endpoint owned by `owner`; it is destroyed with `owner`.
|
||||
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(
|
||||
@@ -181,34 +245,11 @@ impl<P: Platform> Kernel<P> {
|
||||
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> {
|
||||
let core = self.platform.core_id();
|
||||
let tid = self.scheduler.current(core)
|
||||
let tid = self
|
||||
.scheduler
|
||||
.current(core)
|
||||
.ok_or(KernelError::WouldBlock)?;
|
||||
let thread = self.threads.get(tid)?;
|
||||
match thread.state() {
|
||||
@@ -220,11 +261,12 @@ impl<P: Platform> Kernel<P> {
|
||||
pub(crate) fn destroy_process(&mut self, pid: ProcessId) {
|
||||
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) => {
|
||||
let mut ids = [None; MAX_THREADS_PER_PROCESS];
|
||||
for (i, tid) in p.thread_ids().enumerate() {
|
||||
if i < MAX_THREADS_PER_PROCESS { ids[i] = Some(tid); }
|
||||
for (slot, tid) in ids.iter_mut().zip(p.thread_ids()) {
|
||||
*slot = Some(tid);
|
||||
}
|
||||
ids
|
||||
}
|
||||
@@ -232,48 +274,76 @@ impl<P: Platform> Kernel<P> {
|
||||
};
|
||||
|
||||
self.cleanup_threads(&thread_ids);
|
||||
|
||||
// Free allocated pages back to pool
|
||||
if let Some(pm) = &self.process_memory[pid.raw() as usize] {
|
||||
for &(pa, count) in pm.allocations() {
|
||||
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.
|
||||
self.delete_all_caps(pid);
|
||||
self.destroy_endpoints_owned_by(pid);
|
||||
self.destroy_notifications_owned_by(pid);
|
||||
self.teardown_memory(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>]) {
|
||||
use domain::scheduler::Scheduler;
|
||||
for slot in thread_ids {
|
||||
if let Some(tid) = slot {
|
||||
for tid in thread_ids.iter().flatten() {
|
||||
self.endpoints.remove_thread_from_all(*tid);
|
||||
self.cleanup_wait_any(*tid);
|
||||
self.notifications.remove_thread_from_all(*tid);
|
||||
let ipc = self.ipc_mut(*tid).map(core::mem::take).unwrap_or_default();
|
||||
if let Some(caller) = ipc.reply_to {
|
||||
self.release_caller(caller);
|
||||
}
|
||||
self.evict(*tid);
|
||||
self.scheduler.remove(*tid);
|
||||
self.pending_messages[tid.raw() as usize] = None;
|
||||
self.pending_results[tid.raw() as usize] = None;
|
||||
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) {
|
||||
if let Some(wait_set) = self.wait_sets[tid.raw() as usize].take() {
|
||||
for ep_slot in wait_set.endpoints() {
|
||||
if let Some(_slot) = ep_slot {
|
||||
self.endpoints.remove_thread_from_all(tid);
|
||||
break;
|
||||
/// Destroys every notification `owner` created, waking its waiters
|
||||
/// with an error and unbinding its bound thread.
|
||||
pub(crate) fn destroy_notifications_owned_by(&mut self, owner: ProcessId) {
|
||||
let mut owned = [None; MAX_NOTIFICATIONS];
|
||||
for (slot, id) in owned.iter_mut().zip(self.notifications.ids_owned_by(owner)) {
|
||||
*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(
|
||||
@@ -287,43 +357,24 @@ impl<P: Platform> Kernel<P> {
|
||||
if !cap.permissions().allows(needed) {
|
||||
return Err(KernelError::PermissionDenied { slot, needed });
|
||||
}
|
||||
let current_gen = self.resource_generation(cap.resource());
|
||||
if !cap.generation().is_valid_against(¤t_gen) {
|
||||
return 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(),
|
||||
match self.resource_generation(cap.resource()) {
|
||||
Some(current) if cap.generation().is_valid_against(¤t) => Ok(cap),
|
||||
_ => Err(KernelError::RevokedCapability(slot)),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Resource::Endpoint(id) => {
|
||||
if let Some(ep) = self.endpoints.get_mut(*id) {
|
||||
ep.revoke();
|
||||
Resource::Endpoint(id) => self.endpoints.generation(*id),
|
||||
Resource::Memory(id) => self.regions.generation(*id),
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn grant_cap(
|
||||
@@ -332,7 +383,9 @@ impl<P: Platform> Kernel<P> {
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
) -> 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);
|
||||
self.processes.get_mut(to)?.capabilities_mut().insert(cap)
|
||||
}
|
||||
|
||||
486
crates/kernel/src/memory.rs
Normal file
486
crates/kernel/src/memory.rs
Normal 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)
|
||||
}
|
||||
75
crates/kernel/src/proofs/abi.rs
Normal file
75
crates/kernel/src/proofs/abi.rs
Normal 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, ®s) {
|
||||
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);
|
||||
}
|
||||
118
crates/kernel/src/proofs/authority.rs
Normal file
118
crates/kernel/src/proofs/authority.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use domain::capability::{Capability, CapabilityTable};
|
||||
use domain::resource::Resource;
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use types::generation::Generation;
|
||||
use types::id::EndpointId;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
@@ -16,17 +16,16 @@ fn kernel_delegate_path_never_escalates() {
|
||||
kani::assume(source_perms.allows(Permission::Delegate));
|
||||
|
||||
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 original_perms = *cap.permissions();
|
||||
let restricted = original_perms.restrict_to(&PermissionSet::from_bits(restrict_bits));
|
||||
|
||||
let delegated = Capability::new(
|
||||
cap.resource().clone(),
|
||||
restricted,
|
||||
Generation::initial(),
|
||||
);
|
||||
let delegated = Capability::new(*cap.resource(), restricted, Generation::initial());
|
||||
|
||||
let mut target_table = CapabilityTable::new();
|
||||
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();
|
||||
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::Write) { 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)); }
|
||||
if !original_perms.allows(Permission::Revoke) { assert!(!result.permissions().allows(Permission::Revoke)); }
|
||||
if !original_perms.allows(Permission::Read) {
|
||||
assert!(!result.permissions().allows(Permission::Read));
|
||||
}
|
||||
if !original_perms.allows(Permission::Write) {
|
||||
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]
|
||||
@@ -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 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 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 cap_in_b = table_b.get(slot_b).unwrap();
|
||||
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 cap_c = Capability::new(resource, perms_for_c, Generation::initial());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use domain::capability::CapabilityTable;
|
||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||
use domain::capability::CapabilityTable;
|
||||
use types::bounded::CapSlot;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
@@ -38,7 +38,7 @@ fn handle_receive_rejects_without_read_permission() {
|
||||
#[kani::proof]
|
||||
fn missing_cap_always_rejected() {
|
||||
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 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::Write) == perms.allows(Permission::Write));
|
||||
assert!(retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute));
|
||||
assert!(retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate));
|
||||
assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke));
|
||||
assert!(
|
||||
retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute)
|
||||
);
|
||||
assert!(
|
||||
retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
mod abi;
|
||||
mod authority;
|
||||
mod delegation;
|
||||
mod mediation;
|
||||
mod message;
|
||||
mod platform;
|
||||
mod revocation;
|
||||
|
||||
use domain::capability::{Capability, CapabilityTable};
|
||||
|
||||
62
crates/kernel/src/proofs/platform.rs
Normal file
62
crates/kernel/src/proofs/platform.rs
Normal 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) {}
|
||||
}
|
||||
@@ -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 domain::capability::{Capability, CapabilityTable};
|
||||
use domain::resource::Resource;
|
||||
use types::generation::Generation;
|
||||
use types::id::EndpointId;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
@@ -62,77 +61,3 @@ fn grant_cap_preserves_resource_identity() {
|
||||
_ => 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(¤t_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
119
crates/kernel/src/smp.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,18 @@ impl<P: Platform> Kernel<P> {
|
||||
to_endpoint: types::bounded::CapSlot,
|
||||
restrict: types::permission::PermissionSet,
|
||||
) -> SyscallOutcome {
|
||||
let (resource, permissions) = match self.check_cap(
|
||||
caller_pid,
|
||||
slot,
|
||||
types::permission::Permission::Delegate,
|
||||
) {
|
||||
Ok(cap) => (cap.resource().clone(), *cap.permissions()),
|
||||
// A copy of the capability itself, so its badge and generation
|
||||
// travel with it; only the permissions narrow.
|
||||
let copy = match self.check_cap(caller_pid, slot, types::permission::Permission::Delegate) {
|
||||
Ok(cap) => domain::capability::Capability::new(
|
||||
*cap.resource(),
|
||||
cap.permissions().restrict_to(&restrict),
|
||||
*cap.generation(),
|
||||
)
|
||||
.with_badge(cap.badge()),
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
let restricted = permissions.restrict_to(&restrict);
|
||||
|
||||
let target_pid = match self.check_cap(
|
||||
caller_pid,
|
||||
to_endpoint,
|
||||
@@ -29,31 +30,24 @@ impl<P: Platform> Kernel<P> {
|
||||
) {
|
||||
Ok(cap) => match cap.resource() {
|
||||
domain::resource::Resource::Process(pid) => *pid,
|
||||
_ => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
_ => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::PermissionDenied {
|
||||
slot: to_endpoint,
|
||||
needed: types::permission::Permission::Execute,
|
||||
},
|
||||
)),
|
||||
));
|
||||
}
|
||||
},
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -62,16 +56,20 @@ impl<P: Platform> Kernel<P> {
|
||||
caller_pid: types::id::ProcessId,
|
||||
slot: types::bounded::CapSlot,
|
||||
) -> SyscallOutcome {
|
||||
let resource = match self.check_cap(
|
||||
caller_pid,
|
||||
slot,
|
||||
types::permission::Permission::Revoke,
|
||||
) {
|
||||
Ok(cap) => cap.resource().clone(),
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
SyscallOutcome::Complete(match self.revoke_subtree(caller_pid, slot) {
|
||||
Ok(()) => SyscallResult::Ok,
|
||||
Err(e) => SyscallResult::Error(e),
|
||||
})
|
||||
}
|
||||
|
||||
self.revoke_resource(&resource);
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
pub(crate) fn handle_drop_cap(
|
||||
&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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,42 @@
|
||||
use domain::{fault::Fault, message::Message};
|
||||
use platform::Platform;
|
||||
use types::{id::ThreadId, permission::Permission};
|
||||
|
||||
use super::{Delivery, DeliveryKind, SyscallResult};
|
||||
use crate::Kernel;
|
||||
|
||||
impl<P: Platform> Kernel<P> {
|
||||
pub fn handle_fault(
|
||||
&mut self,
|
||||
tid: types::id::ThreadId,
|
||||
fault: domain::fault::Fault,
|
||||
fault_pc: usize,
|
||||
) {
|
||||
let pid = match self.threads.get(tid) {
|
||||
Ok(t) => t.process(),
|
||||
Err(_) => return,
|
||||
/// A thread faulted. Report it to the process's fault endpoint (if one
|
||||
/// is set, valid and has a receiver waiting), then destroy the process.
|
||||
pub fn handle_fault(&mut self, tid: ThreadId, fault: Fault, fault_pc: usize) {
|
||||
let Some(pid) = self.thread_process(tid) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to deliver fault to a fault handler endpoint (if configured)
|
||||
if let Ok(process) = self.processes.get(pid) {
|
||||
if let Some(handler_slot) = process.fault_handler() {
|
||||
if let Ok(cap) = process.capabilities().get(handler_slot) {
|
||||
if let domain::resource::Resource::Endpoint(ep_id) = cap.resource() {
|
||||
let msg = domain::message::Message::from_words([
|
||||
pid.raw() as usize,
|
||||
fault.code(),
|
||||
fault.address(),
|
||||
fault_pc,
|
||||
]);
|
||||
|
||||
if let Some(ep) = self.endpoints.get_mut(*ep_id) {
|
||||
if ep.has_waiting_receiver() {
|
||||
if let Some(receiver_tid) = ep.dequeue_receiver() {
|
||||
self.pending_results[receiver_tid.raw() as usize] =
|
||||
Some(crate::SyscallResult::Message(msg));
|
||||
|
||||
if let Ok(receiver) = self.threads.get_mut(receiver_tid) {
|
||||
let priority = receiver.priority();
|
||||
let _ = receiver.unblock();
|
||||
let core = self.platform.core_id();
|
||||
use domain::scheduler::Scheduler;
|
||||
self.scheduler.add(receiver_tid, priority, core);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let handler = self
|
||||
.processes
|
||||
.get(pid)
|
||||
.ok()
|
||||
.and_then(|p| p.fault_handler())
|
||||
.and_then(|slot| self.check_cap(pid, slot, Permission::Execute).ok())
|
||||
.and_then(|cap| match cap.resource() {
|
||||
domain::resource::Resource::Endpoint(ep) => Some((*ep, cap.badge())),
|
||||
_ => None,
|
||||
});
|
||||
if let Some((ep, badge)) = handler
|
||||
&& let Some(receiver) = self
|
||||
.endpoints
|
||||
.get_mut(ep)
|
||||
.and_then(|e| e.dequeue_receiver())
|
||||
{
|
||||
let report =
|
||||
Message::from_words([pid.raw() as usize, fault.code(), fault.address(), fault_pc]);
|
||||
let delivery = Delivery {
|
||||
kind: DeliveryKind::Message,
|
||||
badge: badge.raw(),
|
||||
words: *report.words(),
|
||||
cap: None,
|
||||
};
|
||||
self.wake(receiver, SyscallResult::Received(delivery));
|
||||
}
|
||||
|
||||
self.destroy_process(pid);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// 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> {
|
||||
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,
|
||||
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 {
|
||||
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,
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
let all_perms = types::permission::PermissionSet::empty()
|
||||
.with(types::permission::Permission::Read)
|
||||
.with(types::permission::Permission::Write)
|
||||
.with(types::permission::Permission::Execute)
|
||||
.with(types::permission::Permission::Delegate);
|
||||
|
||||
match self.grant_cap(
|
||||
caller_pid,
|
||||
domain::resource::Resource::Endpoint(ep_id),
|
||||
all_perms,
|
||||
) {
|
||||
let perms = PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Execute)
|
||||
.with(Permission::Delegate);
|
||||
match self.grant_cap(pid, Resource::Endpoint(id), perms) {
|
||||
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
|
||||
Err(e) => {
|
||||
let _ = self.endpoints.destroy(ep_id);
|
||||
self.endpoints.destroy(id);
|
||||
SyscallOutcome::Complete(SyscallResult::Error(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_send(
|
||||
// ---- notifications ----
|
||||
|
||||
pub fn create_notification(&mut self, owner: ProcessId) -> Result<NotificationId, KernelError> {
|
||||
self.processes.get(owner)?;
|
||||
self.notifications
|
||||
.create(owner)
|
||||
.ok_or(KernelError::NoFreeSlot)
|
||||
}
|
||||
|
||||
pub(crate) fn create_notification_cap(
|
||||
&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;
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
let ep_id = match self.check_cap(
|
||||
caller_pid,
|
||||
endpoint_slot,
|
||||
types::permission::Permission::Execute,
|
||||
) {
|
||||
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)),
|
||||
)),
|
||||
},
|
||||
/// 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)),
|
||||
};
|
||||
|
||||
let endpoint = match self.endpoints.get_mut(ep_id) {
|
||||
Some(ep) => ep,
|
||||
None => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::InvalidEndpoint(ep_id),
|
||||
)),
|
||||
let Some(ntfn) = self.notifications.get_mut(id) else {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(KernelError::NoSuchObject));
|
||||
};
|
||||
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() {
|
||||
let receiver_tid = match endpoint.dequeue_receiver() {
|
||||
Some(t) => t,
|
||||
None => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::WouldBlock,
|
||||
)),
|
||||
pub(crate) fn poll(&mut self, pid: ProcessId, slot: CapSlot) -> Result<usize, KernelError> {
|
||||
let (id, _, _) = self.notification_cap(pid, slot, Permission::Read)?;
|
||||
self.notifications
|
||||
.get_mut(id)
|
||||
.map(|n| n.take_bits())
|
||||
.ok_or(KernelError::NoSuchObject)
|
||||
}
|
||||
|
||||
pub(crate) fn bind_notification(
|
||||
&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(())
|
||||
}
|
||||
|
||||
// ---- interrupts ----
|
||||
|
||||
fn interrupt_cap(&self, pid: ProcessId, slot: CapSlot) -> Result<IrqId, KernelError> {
|
||||
match self.check_cap(pid, slot, Permission::Execute)?.resource() {
|
||||
Resource::Interrupt(irq) => Ok(*irq),
|
||||
_ => Err(KernelError::WrongObject),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_irq_notification(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
irq_slot: CapSlot,
|
||||
ntfn_slot: CapSlot,
|
||||
) -> Result<(), KernelError> {
|
||||
let irq = self.interrupt_cap(pid, irq_slot)?;
|
||||
let (notification, badge, generation) =
|
||||
self.notification_cap(pid, ntfn_slot, Permission::Write)?;
|
||||
let route = domain::irq::IrqRoute {
|
||||
notification,
|
||||
generation,
|
||||
badge,
|
||||
};
|
||||
|
||||
self.pending_results[receiver_tid.raw() as usize] =
|
||||
Some(SyscallResult::Message(message));
|
||||
|
||||
let core = self.platform.core_id();
|
||||
|
||||
let receiver = match self.threads.get_mut(receiver_tid) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
let _ = receiver.unblock();
|
||||
let _ = receiver.set_running(core);
|
||||
|
||||
let caller = match self.threads.get_mut(caller_tid) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
let caller_priority = caller.priority();
|
||||
let _ = caller.preempt();
|
||||
self.scheduler.add(caller_tid, caller_priority, core);
|
||||
|
||||
self.scheduler.set_current(core, receiver_tid);
|
||||
|
||||
SyscallOutcome::SwitchTo(receiver_tid)
|
||||
if self.irqs.set(irq, route) {
|
||||
Ok(())
|
||||
} else {
|
||||
self.pending_messages[caller_tid.raw() as usize] = Some(message);
|
||||
|
||||
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))
|
||||
Err(KernelError::InvalidIrq(irq))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_receive(
|
||||
pub(crate) fn ack_interrupt(
|
||||
&mut self,
|
||||
caller_tid: types::id::ThreadId,
|
||||
caller_pid: types::id::ProcessId,
|
||||
endpoint_slot: types::bounded::CapSlot,
|
||||
) -> SyscallOutcome {
|
||||
use domain::scheduler::Scheduler;
|
||||
pid: ProcessId,
|
||||
slot: CapSlot,
|
||||
) -> Result<(), KernelError> {
|
||||
let irq = self.interrupt_cap(pid, slot)?;
|
||||
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)),
|
||||
/// An interrupt fired: mask the line and signal its notification. The
|
||||
/// line stays masked until the handler acknowledges it.
|
||||
pub fn handle_interrupt(&mut self, irq: IrqId) {
|
||||
self.platform.mask_irq(irq);
|
||||
let Some(route) = self.irqs.route(irq) else {
|
||||
return;
|
||||
};
|
||||
|
||||
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,
|
||||
));
|
||||
// A route set through a capability that has since been revoked, or
|
||||
// to a notification whose slot was reused, is dead.
|
||||
if self.notifications.generation(route.notification) != Some(route.generation) {
|
||||
self.irqs.clear(irq);
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
caller_tid: types::id::ThreadId,
|
||||
caller_pid: types::id::ProcessId,
|
||||
irq_slot: types::bounded::CapSlot,
|
||||
) -> SyscallOutcome {
|
||||
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)),
|
||||
};
|
||||
|
||||
let _ = self.irqs.register(irq_id, caller_tid);
|
||||
|
||||
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::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,
|
||||
));
|
||||
}
|
||||
|
||||
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)
|
||||
self.signal_notification(route.notification, route.badge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use domain::process::MAX_THREADS_PER_PROCESS;
|
||||
use platform::Platform;
|
||||
|
||||
use super::{SyscallOutcome, SyscallResult};
|
||||
@@ -11,14 +10,16 @@ impl<P: Platform> Kernel<P> {
|
||||
entry: types::address::EntryPoint,
|
||||
stack: types::address::StackPointer,
|
||||
) -> SyscallOutcome {
|
||||
use domain::scheduler::Scheduler;
|
||||
use domain::thread::SavedContext;
|
||||
|
||||
let priority = types::bounded::Priority::DEFAULT;
|
||||
let time_slice = types::time::TimeSlice::DEFAULT;
|
||||
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,
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
@@ -28,8 +29,8 @@ impl<P: Platform> Kernel<P> {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
|
||||
let core = self.platform.core_id();
|
||||
self.scheduler.add(tid, priority, core);
|
||||
self.place(tid);
|
||||
self.make_ready(tid, priority);
|
||||
SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize))
|
||||
}
|
||||
|
||||
@@ -39,33 +40,42 @@ impl<P: Platform> Kernel<P> {
|
||||
create_cap: types::bounded::CapSlot,
|
||||
_image: types::address::DataRef,
|
||||
) -> 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));
|
||||
}
|
||||
|
||||
let new_pid = match self.processes.create(
|
||||
domain::capability::CapabilityTable::new(),
|
||||
None,
|
||||
) {
|
||||
let new_pid = match self
|
||||
.processes
|
||||
.create(domain::capability::CapabilityTable::new(), None)
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
self.process_memory[new_pid.raw() as usize] =
|
||||
Some(domain::memory::process_memory::ProcessMemory::new(new_pid, domain::memory::process_memory::ProcessMemory::DEFAULT_PAGE_LIMIT));
|
||||
if let Err(e) = self.create_address_space(new_pid) {
|
||||
let _ = self.processes.destroy(new_pid);
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
|
||||
match self.grant_cap(
|
||||
caller_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()
|
||||
.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)),
|
||||
Err(e) => {
|
||||
// Rollback: destroy orphaned process
|
||||
self.teardown_memory(new_pid);
|
||||
let _ = self.processes.destroy(new_pid);
|
||||
self.process_memory[new_pid.raw() as usize] = None;
|
||||
SyscallOutcome::Complete(SyscallResult::Error(e))
|
||||
}
|
||||
}
|
||||
@@ -78,7 +88,6 @@ impl<P: Platform> Kernel<P> {
|
||||
entry: types::address::EntryPoint,
|
||||
stack: types::address::StackPointer,
|
||||
) -> SyscallOutcome {
|
||||
use domain::scheduler::Scheduler;
|
||||
use domain::thread::SavedContext;
|
||||
|
||||
let target_pid = match self.check_cap(
|
||||
@@ -88,12 +97,14 @@ impl<P: Platform> Kernel<P> {
|
||||
) {
|
||||
Ok(cap) => match cap.resource() {
|
||||
domain::resource::Resource::Process(pid) => *pid,
|
||||
_ => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
_ => {
|
||||
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)),
|
||||
};
|
||||
@@ -102,18 +113,25 @@ impl<P: Platform> Kernel<P> {
|
||||
let time_slice = types::time::TimeSlice::DEFAULT;
|
||||
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,
|
||||
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);
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
|
||||
let core = self.platform.core_id();
|
||||
self.scheduler.add(tid, priority, core);
|
||||
self.place(tid);
|
||||
self.make_ready(tid, priority);
|
||||
SyscallOutcome::Complete(SyscallResult::Value(tid.raw() as usize))
|
||||
}
|
||||
|
||||
@@ -122,20 +140,30 @@ impl<P: Platform> Kernel<P> {
|
||||
caller_pid: types::id::ProcessId,
|
||||
process_cap: types::bounded::CapSlot,
|
||||
) -> 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() {
|
||||
domain::resource::Resource::Process(pid) => *pid,
|
||||
_ => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
_ => {
|
||||
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)),
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +1,135 @@
|
||||
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 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> {
|
||||
pub(crate) fn handle_request_pages(
|
||||
&mut self,
|
||||
pid: types::id::ProcessId,
|
||||
count: types::bounded::PageCount,
|
||||
pid: ProcessId,
|
||||
count: PageCount,
|
||||
) -> SyscallOutcome {
|
||||
let pages = count.value() as usize;
|
||||
|
||||
match &mut self.process_memory[pid.raw() as usize] {
|
||||
Some(pm) => {
|
||||
if pm.grant(pages).is_err() {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
}
|
||||
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)
|
||||
complete(
|
||||
self.find_free_heap_range(pid, pages)
|
||||
.ok_or(KernelError::OutOfMemory)
|
||||
.and_then(|va| {
|
||||
self.map_fresh_range(pid, va, pages, PagePerms::ReadWrite)
|
||||
.map(|()| SyscallResult::Value(va.raw()))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_release_pages(
|
||||
&mut self,
|
||||
pid: types::id::ProcessId,
|
||||
pid: ProcessId,
|
||||
addr: VirtualAddress,
|
||||
count: types::bounded::PageCount,
|
||||
count: PageCount,
|
||||
) -> SyscallOutcome {
|
||||
let pages = count.value() as usize;
|
||||
let phys = match self.platform.translate_va(pid, addr) {
|
||||
Some(pa) => pa,
|
||||
None => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
)),
|
||||
complete(
|
||||
self.release_range(pid, addr, count.value() as usize)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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 region_id = match self.regions.find_by_base(pid, phys) {
|
||||
Some((rid, _)) => rid,
|
||||
None => return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
)),
|
||||
let perms = if executable {
|
||||
PagePerms::ReadExecute
|
||||
} else {
|
||||
PagePerms::ReadWrite
|
||||
};
|
||||
|
||||
if !self.page_pool.free(phys, pages) {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
complete(
|
||||
self.move_page(caller_pid, src_va, target, dst_va, perms)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
)
|
||||
}
|
||||
|
||||
let _ = self.regions.remove(region_id);
|
||||
|
||||
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
|
||||
let _ = pm.release(pages);
|
||||
pub(crate) fn create_region_cap(
|
||||
&mut self,
|
||||
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)
|
||||
}
|
||||
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// The physical base of the region behind `slot`. Needs Read on the
|
||||
/// capability; pool regions are physically contiguous, so the base is
|
||||
/// enough to program a device.
|
||||
pub(crate) fn region_address_cap(
|
||||
&self,
|
||||
pid: ProcessId,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,66 @@ mod lifecycle;
|
||||
mod memory;
|
||||
mod simple;
|
||||
|
||||
use domain::{error::KernelError, message::Message, syscall::Syscall};
|
||||
use domain::{error::KernelError, syscall::Syscall};
|
||||
use platform::Platform;
|
||||
use types::{bounded::CapSlot, id::ThreadId};
|
||||
use types::{bounded::CapSlot, id::ThreadId, syscall_abi::ErrorCode};
|
||||
|
||||
use crate::Kernel;
|
||||
|
||||
pub use ipc::{IpcState, ThreadRef};
|
||||
pub use types::syscall_abi::{Delivery, DeliveryKind};
|
||||
|
||||
pub enum SyscallOutcome {
|
||||
Complete(SyscallResult),
|
||||
BlockCaller(domain::thread::BlockReason),
|
||||
/// The caller blocked and `ThreadId` should run next.
|
||||
SwitchTo(ThreadId),
|
||||
TerminateCaller,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SyscallResult {
|
||||
Ok,
|
||||
Value(usize),
|
||||
Slot(CapSlot),
|
||||
Message(Message),
|
||||
WakeReason(domain::syscall::WakeReason),
|
||||
Received(Delivery),
|
||||
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> {
|
||||
pub fn handle_syscall(&mut self, syscall: Syscall) -> SyscallOutcome {
|
||||
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::ReleasePages { addr, count } => self.handle_release_pages(pid, addr, count),
|
||||
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::Revoke { slot } => self.handle_revoke(pid, slot),
|
||||
Syscall::Delegate { slot, to_endpoint, permissions } => {
|
||||
self.handle_delegate(pid, slot, to_endpoint, permissions)
|
||||
Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),
|
||||
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::Receive { endpoint } => self.handle_receive(tid, pid, endpoint),
|
||||
Syscall::WaitForInterrupt { irq } => self.handle_wait_for_interrupt(tid, pid, irq),
|
||||
Syscall::AcknowledgeInterrupt { irq } => self.handle_ack_interrupt(pid, irq),
|
||||
Syscall::WaitAny { sources } => self.handle_wait_any(tid, pid, sources),
|
||||
Syscall::Halt => self.handle_halt(pid),
|
||||
Syscall::MapRegion { region, va, perms } => complete(
|
||||
self.map_region_cap(pid, region, va, perms)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
),
|
||||
Syscall::RegionAddress { region } => complete(
|
||||
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::DebugPutchar { byte } => self.handle_debug_putchar(byte),
|
||||
Syscall::MapPage { process, src_va, dst_va, 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)
|
||||
}
|
||||
Syscall::MapPage {
|
||||
process,
|
||||
src_va,
|
||||
dst_va,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use domain::{error::KernelError, resource::Resource};
|
||||
use platform::Platform;
|
||||
use types::{bounded::CapSlot, id::ProcessId, permission::Permission};
|
||||
|
||||
use super::{SyscallOutcome, SyscallResult};
|
||||
use crate::Kernel;
|
||||
@@ -31,15 +33,26 @@ impl<P: Platform> Kernel<P> {
|
||||
|
||||
match self.scheduler.next(core) {
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -59,10 +72,7 @@ impl<P: Platform> Kernel<P> {
|
||||
caller_tid: types::id::ThreadId,
|
||||
pid: types::id::ProcessId,
|
||||
) -> SyscallOutcome {
|
||||
use domain::scheduler::Scheduler;
|
||||
|
||||
self.scheduler.remove(caller_tid);
|
||||
let _ = self.threads.destroy(caller_tid);
|
||||
self.cleanup_threads(&[Some(caller_tid)]);
|
||||
|
||||
if let Ok(process) = self.processes.get_mut(pid) {
|
||||
process.remove_thread(caller_tid);
|
||||
@@ -79,15 +89,13 @@ impl<P: Platform> Kernel<P> {
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_halt(
|
||||
&mut self,
|
||||
pid: types::id::ProcessId,
|
||||
) -> SyscallOutcome {
|
||||
if pid.raw() != 0 {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::ProcessCreateDenied,
|
||||
));
|
||||
}
|
||||
self.platform.halt();
|
||||
/// Halts the machine for a holder of a SystemControl capability with
|
||||
/// Execute; anyone else gets an error and nothing happens.
|
||||
pub(crate) fn handle_halt(&mut self, pid: ProcessId, control: CapSlot) -> SyscallOutcome {
|
||||
match self.check_cap(pid, control, Permission::Execute) {
|
||||
Ok(cap) if *cap.resource() == Resource::SystemControl => self.platform.halt(),
|
||||
Ok(_) => SyscallOutcome::Complete(SyscallResult::Error(KernelError::WrongObject)),
|
||||
Err(e) => SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
crates/kernel/src/tests/abi.rs
Normal file
100
crates/kernel/src/tests/abi.rs
Normal 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);
|
||||
}
|
||||
323
crates/kernel/src/tests/board.rs
Normal file
323
crates/kernel/src/tests/board.rs
Normal 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));
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
use super::*;
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::resource::Resource;
|
||||
use domain::syscall::Syscall;
|
||||
use types::bounded::CapSlot;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::address::DataRef;
|
||||
use types::bounded::CapSlot;
|
||||
use types::permission::PermissionSet;
|
||||
|
||||
#[test]
|
||||
fn delegate_without_delegate_permission_fails() {
|
||||
@@ -58,131 +57,16 @@ fn delegate_with_valid_cap_succeeds() {
|
||||
// --- Generation-based revocation ---
|
||||
|
||||
#[test]
|
||||
fn revoke_resource_invalidates_all_caps() {
|
||||
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),
|
||||
fn every_representable_slot_is_rejected_not_panicking() {
|
||||
let (mut k, _) = kernel_with_process();
|
||||
for raw in 1..=CapSlot::MAX {
|
||||
let outcome = k.handle_syscall(Syscall::Send {
|
||||
endpoint: CapSlot::new(raw).unwrap(),
|
||||
message: domain::message::Message::empty(),
|
||||
});
|
||||
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(_))
|
||||
));
|
||||
}
|
||||
|
||||
326
crates/kernel/src/tests/captree_model.rs
Normal file
326
crates/kernel/src/tests/captree_model.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use crate::SyscallOutcome;
|
||||
use domain::syscall::Syscall;
|
||||
|
||||
#[test]
|
||||
@@ -21,5 +21,5 @@ fn exit_thread_last_thread_destroys_process() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
k.handle_syscall(Syscall::ExitThread);
|
||||
// Process should be gone since it had only one thread
|
||||
assert!(k.process_exists(pid) == false);
|
||||
assert!(!k.process_exists(pid));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use domain::memory::page_pool::PagePool;
|
||||
use domain::resource::Resource;
|
||||
use domain::scheduler::Scheduler;
|
||||
use domain::thread::ThreadState;
|
||||
@@ -6,25 +5,22 @@ use platform::Platform;
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
bounded::{CapSlot, Priority},
|
||||
id::{EndpointId, IrqId, ProcessId, ThreadId},
|
||||
id::{EndpointId, ProcessId, ThreadId},
|
||||
permission::PermissionSet,
|
||||
time::TimeSlice,
|
||||
};
|
||||
|
||||
use crate::Kernel;
|
||||
use super::mock_platform::MockPlatform;
|
||||
use crate::Kernel;
|
||||
|
||||
pub fn test_kernel() -> Kernel<MockPlatform> {
|
||||
let mock = MockPlatform::new(1);
|
||||
let pool = PagePool::new(PhysicalAddress::from_raw(0x8000_0000), 1024);
|
||||
Kernel::new(mock, pool)
|
||||
Kernel::new(mock, PhysicalAddress::from_raw(0x8400_0000), 1024)
|
||||
}
|
||||
|
||||
pub fn kernel_with_process() -> (Kernel<MockPlatform>, ProcessId) {
|
||||
let mut kernel = test_kernel();
|
||||
let (pid, _tid) = kernel
|
||||
.boot(Priority::DEFAULT, TimeSlice::DEFAULT)
|
||||
.unwrap();
|
||||
let (pid, _tid) = kernel.boot(Priority::DEFAULT, TimeSlice::DEFAULT).unwrap();
|
||||
(kernel, pid)
|
||||
}
|
||||
|
||||
@@ -35,22 +31,18 @@ impl Kernel<MockPlatform> {
|
||||
priority: Priority,
|
||||
time_slice: TimeSlice,
|
||||
) -> 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 core = self.platform.core_id();
|
||||
self.set_home(tid, core);
|
||||
self.scheduler.add(tid, priority, core);
|
||||
tid
|
||||
}
|
||||
|
||||
pub fn register_irq_for_test(&mut self, irq: IrqId, thread: ThreadId) {
|
||||
let _ = self.irqs.register(irq, thread);
|
||||
}
|
||||
|
||||
pub fn block_thread_for_test(
|
||||
&mut self,
|
||||
tid: ThreadId,
|
||||
reason: domain::thread::BlockReason,
|
||||
) {
|
||||
pub fn block_thread_for_test(&mut self, tid: ThreadId, reason: domain::thread::BlockReason) {
|
||||
let core = self.platform.core_id();
|
||||
if let Ok(t) = self.threads.get_mut(tid) {
|
||||
if matches!(t.state(), ThreadState::Ready) {
|
||||
@@ -63,30 +55,44 @@ impl Kernel<MockPlatform> {
|
||||
|
||||
pub fn switch_to_thread_for_test(&mut self, tid: ThreadId) {
|
||||
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 current == tid {
|
||||
if current == tid || !switchable {
|
||||
return;
|
||||
}
|
||||
if let Ok(t) = self.threads.get_mut(current) {
|
||||
if matches!(t.state(), ThreadState::Running(_)) {
|
||||
if let Ok(t) = self.threads.get_mut(current)
|
||||
&& 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);
|
||||
if let Ok(t) = self.threads.get(tid)
|
||||
&& matches!(t.state(), ThreadState::Ready)
|
||||
&& self.scheduler.run_now(core, tid)
|
||||
{
|
||||
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -95,6 +101,95 @@ impl Kernel<MockPlatform> {
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
) -> 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())
|
||||
}
|
||||
}
|
||||
|
||||
299
crates/kernel/src/tests/invariants.rs
Normal file
299
crates/kernel/src/tests/invariants.rs
Normal 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
Reference in New Issue
Block a user