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;

View File

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

View File

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