fix: stacks at top of user VA in own 1 MiB band (guard, heap kept below), 8/16 pages default; kernel fault msg names pid, kind, flags stack overflow; flaky OVERFLOW + spawntest/test-qemu check it

This commit is contained in:
2026-10-28 16:03:06 +01:00
parent 18e8b3ecb8
commit 495bf03484
12 changed files with 93 additions and 22 deletions

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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(())
}

View File

@@ -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 {

View File

@@ -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<Lo
let process_cap = syscall::create_process(CREATE_CAP, limits)?;
let filled = elf_info.segments().all(|segment| load_segment(file, process_cap, segment).is_some())
&& (0..stack_pages).all(|i| {
let dst = VirtualAddress::from_raw(USER_CODE_BASE - (stack_pages - i) * PAGE_SIZE);
let dst = VirtualAddress::from_raw(types::layout::STACK_TOP - (stack_pages - i) * PAGE_SIZE);
PageCount::new(1).and_then(syscall::request_pages).is_some_and(|va| {
syscall::map_page(process_cap, va, dst, false);
true

View File

@@ -72,7 +72,14 @@ impl<P: Platform> Kernel<P> {
fn first_fit_from(&self, pid: ProcessId, from: usize, pages: usize) -> Option<VirtualAddress> {
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),

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;