From b814bd957052d6fcd1099c313e486ae16370d9bc Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 12:54:42 +0100 Subject: [PATCH] test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu --- Cargo.lock | 8 +++ Cargo.toml | 1 + Makefile | 10 ++- crates/apps/smptest/Cargo.toml | 13 ++++ crates/apps/smptest/src/main.rs | 51 ++++++++++++++ crates/init/src/devices.rs | 19 ++++- crates/init/src/lib.rs | 20 ++++-- crates/init/src/selftest.rs | 119 ++++++++++++++++++++++++++++++++ scripts/make-disk-image.sh | 6 +- scripts/test-qemu.sh | 27 ++++++++ 10 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 crates/apps/smptest/Cargo.toml create mode 100644 crates/apps/smptest/src/main.rs create mode 100644 crates/init/src/selftest.rs create mode 100755 scripts/test-qemu.sh diff --git a/Cargo.lock b/Cargo.lock index c3dd30c..f30cdc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,14 @@ dependencies = [ "types", ] +[[package]] +name = "smptest" +version = "0.1.0" +dependencies = [ + "libkos", + "types", +] + [[package]] name = "snake" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8b6d481..2dd7e05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/adapter-riscv", "crates/apps/hello", "crates/apps/echo", + "crates/apps/smptest", "crates/apps/snake", "crates/domain", "crates/elf", diff --git a/Makefile b/Makefile index 6923e70..10fb754 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test +.PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test test-qemu RISCV_TARGET = riscv64gc-unknown-none-elf KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv @@ -52,11 +52,17 @@ build: cargo build --target $(RISCV_TARGET) --release \ -p adapter-riscv -p init -p echo -p snake \ -p display-qemu -p input-qemu -p storage-qemu \ - -p log-server -p permission-manager -p session-server + -p log-server -p permission-manager -p session-server -p smptest image: build scripts/make-disk-image.sh +# Boots a test image (normal image + smptest.elf) on $(SMP) harts; init runs +# the SMP self-test and halts. Passes only on "SMPTEST PASS". +test-qemu: build + IMG=test.img SMPTEST=1 scripts/make-disk-image.sh > /dev/null + SMP=$(SMP) scripts/test-qemu.sh test.img + run: build qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \ -kernel $(KERNEL_BIN) diff --git a/crates/apps/smptest/Cargo.toml b/crates/apps/smptest/Cargo.toml new file mode 100644 index 0000000..7491d41 --- /dev/null +++ b/crates/apps/smptest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "smptest" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "smptest" +test = false +bench = false + +[dependencies] +types = { workspace = true } +libkos = { workspace = true } diff --git a/crates/apps/smptest/src/main.rs b/crates/apps/smptest/src/main.rs new file mode 100644 index 0000000..454096e --- /dev/null +++ b/crates/apps/smptest/src/main.rs @@ -0,0 +1,51 @@ +//! The spinner half of init's SMP self-test (see init's `selftest`): +//! maps the counter page it was given in slot 0 and runs one thread per +//! hart, each incrementing its own counter for ever. Init watches the +//! counters through its own mapping of the same page. + +#![no_std] +#![no_main] + +extern crate libkos; + +use core::sync::atomic::{AtomicUsize, Ordering}; +use libkos::syscall; +use types::address::{EntryPoint, StackPointer, VirtualAddress}; +use types::bounded::{CapSlot, PageCount}; +use types::mapping::PagePerms; + +/// Where the counter page is mapped. Layout (words): 0 the next free +/// counter index, 1..=THREADS the counters. +const COUNTERS_VA: usize = 0x0100_0000; +const THREADS: usize = 4; + +#[unsafe(no_mangle)] +extern "C" fn main() { + let page = CapSlot::const_new(0); + if syscall::map_region(page, VirtualAddress::from_raw(COUNTERS_VA), PagePerms::ReadWrite).is_err() { + libkos::println!("smptest: cannot map the counter page"); + syscall::exit(); + } + for _ in 1..THREADS { + let Some(stack) = PageCount::new(1).and_then(syscall::request_pages) else { + break; + }; + let top = StackPointer::from_raw(stack.raw() + 4096); + syscall::spawn_thread(EntryPoint::from_raw(spin as *const () as usize), top); + } + spin(); +} + +extern "C" fn spin() -> ! { + // SAFETY: COUNTERS_VA maps the counter page; every access is atomic. + let word = |i: usize| unsafe { &*((COUNTERS_VA + i * 8) as *const AtomicUsize) }; + let mine = 1 + word(0).fetch_add(1, Ordering::Relaxed) % THREADS; + loop { + // Yield now and then: a spinner sharing init's hart would + // otherwise starve it until the next (1 s) timer tick. + for _ in 0..1024 { + word(mine).fetch_add(1, Ordering::Relaxed); + } + syscall::yield_now(); + } +} diff --git a/crates/init/src/devices.rs b/crates/init/src/devices.rs index 88e7c9e..b39f796 100644 --- a/crates/init/src/devices.rs +++ b/crates/init/src/devices.rs @@ -49,11 +49,24 @@ fn virtio_id(region: CapSlot) -> u32 { id } +fn boot_info() -> &'static BootInfo { + // SAFETY: boot maps the BootInfo page read-only at BOOT_INFO_VA before + // init runs; readers check its magic. + unsafe { &*(BOOT_INFO_VA as *const BootInfo) } +} + +/// The capability that halts the machine, if boot said where it is. +pub fn system_control() -> Option { + let info = boot_info(); + if info.magic != types::bootinfo::MAGIC { + return None; + } + slot(info.system_control_slot) +} + /// Sorts the board's devices out. pub fn discover() -> Devices { - // SAFETY: boot maps the BootInfo page read-only at BOOT_INFO_VA before - // init runs; `devices()` checks its magic. - let info = unsafe { &*(BOOT_INFO_VA as *const BootInfo) }; + let info = boot_info(); let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE }; let mut uart = Device::NONE; for d in info.devices() { diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index d0137a6..499fa45 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; mod devices; +mod selftest; use types::address::{EntryPoint, StackPointer, VirtualAddress}; use types::bounded::{CapSlot, PageCount}; @@ -12,7 +13,7 @@ use libkos::syscall; const PAGE_SIZE: usize = 4096; const USER_CODE_BASE: usize = 0x10000; -const USER_STACK_TOP: StackPointer = StackPointer::from_raw(USER_CODE_BASE); +pub(crate) const USER_STACK_TOP: StackPointer = StackPointer::from_raw(USER_CODE_BASE); const USER_STACK_PAGES: usize = 4; /// Where a program file is read to before it is loaded (16 pages: the /// largest program init will load). @@ -36,12 +37,12 @@ fn read_file(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Opti read.filter(|r| Some(*r) == size) } -struct LoadedProcess { - process_cap: CapSlot, - entry_point: EntryPoint, +pub(crate) struct LoadedProcess { + pub(crate) process_cap: CapSlot, + pub(crate) entry_point: EntryPoint, } -fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option { +pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option { let file_size = read_file(storage, scratch, filename)?; let elf_data = &scratch[..file_size]; let elf_info = match elf::parse(elf_data) { @@ -156,6 +157,13 @@ pub fn run() -> ! { libkos::println!("ok (entry={:#x})", loaded.entry_point.raw()); } + // The test image carries the SMP self-test; it runs instead of the + // desktop and halts the machine. + if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) { + libkos::println!("init: running the SMP self-test"); + selftest::run(&mut storage, scratch); + } + libkos::print!("init: loading snake... "); if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") { // A client only calls: Execute, never Read, so it cannot pose as @@ -189,7 +197,7 @@ fn scratch() -> Option<&'static mut [u8]> { /// Nothing to supervise yet: block on a notification nobody signals /// rather than spin a core on Yield. -fn idle() -> ! { +pub(crate) fn idle() -> ! { let idle = syscall::create_notification(); loop { match idle { diff --git a/crates/init/src/selftest.rs b/crates/init/src/selftest.rs new file mode 100644 index 0000000..50db340 --- /dev/null +++ b/crates/init/src/selftest.rs @@ -0,0 +1,119 @@ +//! SMP self-test, run only when the disk carries `smptest.elf` (the test +//! image built by `make test-qemu`). It checks on real harts what host +//! tests cannot: that unmapping a page and killing a process take effect +//! on every other hart before the call returns. +//! +//! A spinner process runs one thread per hart, each incrementing its own +//! counter in a page init also maps. Init then cuts the spinner off +//! (revoking its copy of the page, or killing it) and requires that no +//! counter moves afterwards: a stale TLB entry or a thread left running +//! on another hart would keep counting. + +use core::sync::atomic::{AtomicUsize, Ordering}; +use libkos::fs::Storage; +use libkos::syscall; +use types::address::VirtualAddress; +use types::bounded::{CapSlot, PageCount}; +use types::mapping::PagePerms; +use types::permission::{Permission, PermissionSet}; + +/// Where init sees the counter page; same layout as the spinner's. +const OBSERVE_VA: usize = 0x0140_0000; +const THREADS: usize = 4; +/// QEMU virt's timebase is 10 MHz. +const TICKS_PER_MS: u64 = 10_000; + +fn word(i: usize) -> &'static AtomicUsize { + // SAFETY: OBSERVE_VA maps the counter page for the whole test. + unsafe { &*((OBSERVE_VA + i * 8) as *const AtomicUsize) } +} + +fn counters() -> [usize; THREADS] { + core::array::from_fn(|i| word(1 + i).load(Ordering::SeqCst)) +} + +/// Spins (yielding) until `done` or `ms` milliseconds pass. +fn wait_until(ms: u64, mut done: impl FnMut() -> bool) -> bool { + let deadline = syscall::get_time().raw() + ms * TICKS_PER_MS; + while syscall::get_time().raw() < deadline { + if done() { + return true; + } + syscall::yield_now(); + } + done() +} + +/// Starts a spinner on `page`; its process capability. +fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot) -> Result { + for i in 0..=THREADS { + word(i).store(0, Ordering::SeqCst); + } + let spinner = crate::load_binary(storage, scratch, b"smptest.elf").ok_or("cannot load smptest.elf")?; + let rw = PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Write) + .with(Permission::Delegate); + if !syscall::delegate(page, spinner.process_cap, rw) { + return Err("cannot give the spinner its page"); + } + syscall::spawn_thread_in(spinner.process_cap, spinner.entry_point, crate::USER_STACK_TOP); + if !wait_until(3000, || counters().iter().all(|c| *c > 0)) { + return Err("spinner threads did not all start"); + } + Ok(spinner.process_cap) +} + +/// After `stop`, no counter may move. +fn check_stopped(what: &'static str) -> Result<(), &'static str> { + let before = counters(); + wait_until(200, || false); + if counters() != before { + return Err(what); + } + Ok(()) +} + +fn run_phases(storage: &mut Storage, scratch: &mut [u8]) -> Result<(), &'static str> { + let page = syscall::create_region(PageCount::new(1).ok_or("page count")?).map_err(|_| "no counter page")?; + syscall::map_region(page, VirtualAddress::from_raw(OBSERVE_VA), PagePerms::ReadWrite) + .map_err(|_| "cannot map the counter page")?; + + // 1. Unmapping: revoking the spinner's copy of the page must unmap it + // on every hart before Revoke returns. The spinner then faults, + // which kills it from whichever hart faults first. + let spinner = start_spinner(storage, scratch, page)?; + if !syscall::revoke(page) { + return Err("revoke failed"); + } + check_stopped("a counter moved after its page was revoked (stale TLB)")?; + if syscall::kill(spinner) { + return Err("the spinner survived losing its page"); + } + libkos::println!("smptest: revoke stops every hart: ok"); + + // 2. Killing: a process whose threads are running on other harts must + // stop everywhere before Kill returns. + let spinner = start_spinner(storage, scratch, page)?; + if !syscall::kill(spinner) { + return Err("kill failed"); + } + check_stopped("a counter moved after its process was killed")?; + libkos::println!("smptest: kill stops every hart: ok"); + Ok(()) +} + +/// Runs the test, reports, and halts the machine. +pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! { + match run_phases(storage, scratch) { + Ok(()) => libkos::println!("SMPTEST PASS"), + Err(why) => libkos::println!("SMPTEST FAIL: {}", why), + } + match crate::devices::system_control() { + Some(control) => syscall::halt(control), + None => { + libkos::println!("smptest: no SystemControl capability to halt with"); + crate::idle() + } + } +} diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index 6591a4e..8878d2b 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -IMG="boot.img" +IMG="${IMG:-boot.img}" IMG_SIZE_KB=4096 TARGET="riscv64gc-unknown-none-elf" BUILD_DIR="target/${TARGET}/release" @@ -30,6 +30,10 @@ copy log-server "log.elf" copy permission-manager "permis.elf" copy storage-qemu "storage.elf" copy session-server "session.elf" +# Only the test image: its presence makes init run the SMP self-test. +if [ -n "${SMPTEST:-}" ]; then + copy smptest "smptest.elf" +fi echo "" echo "Disk contents:" diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh new file mode 100755 index 0000000..bb44d1b --- /dev/null +++ b/scripts/test-qemu.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Boots IMAGE headless and waits for init's SMP self-test to report; the +# machine halts itself when done. Exit 0 only on "SMPTEST PASS". +set -uo pipefail + +IMAGE="${1:?usage: test-qemu.sh IMAGE}" +SMP="${SMP:-4}" +KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv" +LOG="$(mktemp -t kos-test-qemu.XXXXXX)" + +timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \ + -kernel "${KERNEL}" \ + -drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \ + -device virtio-gpu-device -device virtio-keyboard-device \ + -display none -serial "file:${LOG}" -monitor none +status=$? + +grep -a "smptest:\|SMPTEST" "${LOG}" +if grep -aq "SMPTEST PASS" "${LOG}"; then + echo "test-qemu: pass (${SMP} harts)" + rm -f "${LOG}" + exit 0 +fi +[ "${status}" -eq 124 ] && echo "test-qemu: timed out" +echo "test-qemu: FAIL, last serial output (${LOG}):" +tail -20 "${LOG}" +exit 1