diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 3646008..096919b 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -182,17 +182,29 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) { .is_some_and(|ctx| fp::first_use(frame, ctx)) => {} _ => { let uart = crate::hw::uart::Uart::new(); - uart.write_str("FAULT: scause="); - uart.write_hex(scause); let stval: usize; unsafe { core::arch::asm!("csrr {0}, stval", out(reg) stval) }; - uart.write_str(" stval="); + let kern = kern(); + uart.write_str("FAULT pid="); + let pid = kern.current_thread().and_then(|t| kern.thread_process(t)); + uart.write_hex(pid.map_or(usize::MAX, |p| p.raw() as usize)); + uart.write_str(": "); + uart.write_str(match scause { + 2 => "illegal instruction", + 5 | 13 => "load fault", + 7 | 15 => "store fault", + 12 => "instruction fetch fault", + _ => "fault", + }); + uart.write_str(" at "); uart.write_hex(stval); - uart.write_str(" sepc="); + if matches!(scause, 5 | 7 | 13 | 15) && types::layout::is_stack_overflow(stval) { + uart.write_str(" (stack overflow: give it more stack)"); + } + uart.write_str(", pc "); uart.write_hex(frame.sepc); uart.put_char(b'\n'); - let kern = kern(); if let Some(current_tid) = kern.current_thread() { let fault = match scause { 2 => domain::fault::Fault::IllegalInstruction, diff --git a/crates/apps/flaky/src/lib.rs b/crates/apps/flaky/src/lib.rs index 9b8b02d..2f787ba 100644 --- a/crates/apps/flaky/src/lib.rs +++ b/crates/apps/flaky/src/lib.rs @@ -16,6 +16,16 @@ pub mod op { pub const CRASH: usize = 2; /// Blocks for good inside the handler. pub const HANG: usize = 3; + /// Recurses until its stack runs out: the kernel says it overflowed. + pub const OVERFLOW: usize = 4; +} + +/// Ever deeper, a KiB of stack a call, until there is none. +#[allow(unconditional_recursion, reason = "running off the stack is what it is for")] +fn deeper(n: usize) -> usize { + let frame = core::hint::black_box([n as u8; 1024]); + // Used after the call, so the call is not a jump. + deeper(n + 1) + frame[n % 1024] as usize } /// Everything goes through `handle_call`. @@ -49,6 +59,9 @@ impl Server for Flaky { match words[0] { op::WHO => {} op::CRASH => panic!("asked to crash"), + op::OVERFLOW => { + core::hint::black_box(deeper(0)); + } op::HANG => { // A notification nobody signals. if let Ok(never) = syscall::create_notification() { diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index 802e32f..09557db 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -267,6 +267,16 @@ fn restarted_after_hanging() -> Result<(), &'static str> { Ok(()) } +/// A server that runs off its stack faults on the guard below it, and is +/// restarted like any other that died. +fn restarted_after_overflowing() -> Result<(), &'static str> { + let _ = syscall::request(FLAKY, [flaky::op::OVERFLOW, 0, 0, 0]); + if flaky()? != 1 { + return Err("the overflowed server was not restarted"); + } + Ok(()) +} + fn check() -> Result<(), &'static str> { let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?; write_a_file(&mut fs)?; @@ -305,6 +315,9 @@ fn check() -> Result<(), &'static str> { restarted_after_hanging()?; libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok"); + + restarted_after_overflowing()?; + libkos::println!("spawntest: a server that overflows its stack is caught and restarted: ok"); Ok(()) } diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index ef35be9..385a785 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -17,7 +17,7 @@ use crate::manifest::{Device, Program, Scope}; use crate::spawn::Spawner; use crate::supervise::{MAX_SUPERVISED, Supervised}; -const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000); +const USER_STACK_TOP: StackPointer = StackPointer::from_raw(types::layout::STACK_TOP); const MAX_SERVED: usize = 8; fn perms(list: &[Permission]) -> PermissionSet { diff --git a/crates/init/src/load.rs b/crates/init/src/load.rs index 760f30b..89f69ef 100644 --- a/crates/init/src/load.rs +++ b/crates/init/src/load.rs @@ -7,14 +7,12 @@ use types::bounded::{CapSlot, PageCount}; use types::budget::Limits; const PAGE_SIZE: usize = 4096; -const USER_CODE_BASE: usize = 0x10000; -/// A program's stack, below its image, unless its manifest says more. -pub(crate) const STACK_PAGES: usize = 4; +/// A program's stack (see `types::layout`), unless init's table says more. +pub(crate) const STACK_PAGES: usize = 8; /// An app's: drawing (tiny-skia, fontdue) takes more than 16 KiB. -pub(crate) const APP_STACK_PAGES: usize = 8; -/// The most there is room for under the image, above the arguments page -/// (page 0 stays unmapped). -const MAX_STACK_PAGES: usize = (USER_CODE_BASE - types::start_arg::ARGS_VA) / PAGE_SIZE - 1; +pub(crate) const APP_STACK_PAGES: usize = 16; +/// The most the stack's band holds, leaving a guard page at its bottom. +const MAX_STACK_PAGES: usize = types::layout::STACK_BAND / PAGE_SIZE - 1; const CREATE_CAP: CapSlot = CapSlot::const_new(0); /// Where a program file is read through on its way into a process, a /// chunk at a time, so a program may be any size. @@ -102,7 +100,7 @@ fn load_from(file: &mut Stream, limits: Limits, stack_pages: usize) -> Option Kernel

{ fn first_fit_from(&self, pid: ProcessId, from: usize, pages: usize) -> Option { let mut candidate = from; - while Self::in_user_window(VirtualAddress::from_raw(candidate), pages) { + // Never into the stack's band (`types::layout`). + let below_stacks = |c: usize| { + c.checked_add(pages * PAGE_SIZE) + .is_some_and(|end| end <= types::layout::HEAP_END) + }; + while Self::in_user_window(VirtualAddress::from_raw(candidate), pages) + && below_stacks(candidate) + { let va = VirtualAddress::from_raw(candidate); match (0..pages).find(|&i| self.is_mapped(pid, page_at(va, i))) { None => return Some(va), diff --git a/crates/types/src/layout.rs b/crates/types/src/layout.rs new file mode 100644 index 0000000..84ae94a --- /dev/null +++ b/crates/types/src/layout.rs @@ -0,0 +1,18 @@ +//! Where things sit in every program's address space, below 1 GiB (the +//! RISC-V adapter's user window): the arguments page at `start_arg:: +//! ARGS_VA`, the image from 0x10000, the heap above it (wherever the +//! kernel finds room, below `HEAP_END`), and the stack at the very top, +//! in a band of its own. The band's pages below the stack are never +//! mapped, so running off the stack faults there, and the kernel says so. + +/// The first thread's stack ends here (the top of the user window). +pub const STACK_TOP: usize = 0x4000_0000; +/// The band the stack grows down into: its unmapped rest is the guard. +pub const STACK_BAND: usize = 0x10_0000; +/// The heap stays below the stack's band. +pub const HEAP_END: usize = STACK_TOP - STACK_BAND; + +/// Whether a fault at `va` is a stack running off its end. +pub const fn is_stack_overflow(va: usize) -> bool { + va >= HEAP_END && va < STACK_TOP +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 742a856..095a248 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -12,6 +12,7 @@ pub mod gatekeeper; pub mod generation; pub mod id; pub mod input; +pub mod layout; pub mod log; pub mod macros; pub mod mapping; diff --git a/crates/types/src/spawn.rs b/crates/types/src/spawn.rs index ea96628..b0b7ce4 100644 --- a/crates/types/src/spawn.rs +++ b/crates/types/src/spawn.rs @@ -12,8 +12,8 @@ use crate::budget::Limits; pub const SPAWN: usize = 0; -/// Where init puts the child's stack top (its stack ends at the image). -pub const STACK_TOP: usize = 0x10000; +/// Where init puts the child's stack top (see `layout`). +pub const STACK_TOP: usize = crate::layout::STACK_TOP; /// A slot word meaning "none". pub const NONE: usize = usize::MAX; diff --git a/crates/types/src/start_arg.rs b/crates/types/src/start_arg.rs index e7c3287..6f49a4e 100644 --- a/crates/types/src/start_arg.rs +++ b/crates/types/src/start_arg.rs @@ -14,7 +14,8 @@ const PERMISSIONS_SHIFT: u32 = 48; const SESSION_SHIFT: u32 = 56; /// Where the arguments page sits: the program's arguments, NUL separated, -/// the first its name. Below the stack, which ends at the image. +/// the first its name. Above the unmapped page 0, below the image (see +/// `layout`). pub const ARGS_VA: usize = 0x1000; pub const ARGS_SIZE: usize = 4096; diff --git a/docs/ipc.md b/docs/ipc.md index 17f5eee..9976906 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -79,9 +79,12 @@ the slots of the console, log and storage capabilities it was handed, and whether its parent put an arguments page at `ARGS_VA` (NUL-separated, the program's name first; the parent fills a page and `MapPage`s it over, so the program owns it and it goes when the program does). It sits at -`0x1000`, above the unmapped page 0; the stack runs from just below the -image (`0x10000`) down toward it: 4 pages for system programs unless init's -table says more, 8 for apps. +`0x1000`, above the unmapped page 0, below the image (`0x10000`). The +stack is at the top of the user window (`types::layout`): it ends at +`STACK_TOP` (1 GiB) in a 1 MiB band of its own, 8 pages for system +programs unless init's table says more, 16 for apps; the rest of the band +is never mapped (the heap stays below it), so running off the stack +faults there and the kernel prints `stack overflow` with the process id. Boot loads only init and storage, both packed into the kernel image (they are what reads the disk, so they cannot come from it). The disk is one ext4 diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index d6792df..56fc106 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -57,6 +57,11 @@ echo "test-qemu: second boot: wrong passcode refused, right one signs in, sealed rm -f "${KEYS}" "${MON}" grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" +# The kernel names a stack overflow (flaky's, asked for) as one. +overflow_ok=1 +if ! grep -aq "(stack overflow" "${LOG}"; then + echo "test-qemu: a stack overflow was not reported as one"; overflow_ok=0 +fi # What k-os wrote must be ext4 that the real tools accept. disk_ok=1 if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then @@ -81,7 +86,7 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then fi rm -f "${LOG}.fsck" fi -if [ "${disk_ok}" -eq 1 ] && [ ${first_ok} -eq 1 ] && [ ${second_ok} -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then +if [ "${disk_ok}" -eq 1 ] && [ ${overflow_ok} -eq 1 ] && [ ${first_ok} -eq 1 ] && [ ${second_ok} -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then echo "test-qemu: pass (${SMP} harts)" rm -f "${LOG}" exit 0