Stack takes an explicit (reserve, guard) shape, both page-rounded and
stored; usable_base derives from the stored guard. Guard default raised
4 KiB -> 64 KiB (DEFAULT_STACK_GUARD): probestack makes one page enough
for Rust frames, but an unprobed C frame can leap a page in one sub rsp
— the motivating SQLite segfault. Reserve default stays 64 KiB
(DEFAULT_STACK_RESERVE); ACTOR_STACK_SIZE retired.
Config::{stack_reserve, stack_guard} thread the runtime defaults into
RuntimeInner pre-rounded. All acquisition/recycling now goes through
acquire_stack/recycle_stack carrying the pool rule: only default-shaped
stacks are pooled (pooled ⇒ default-shaped by induction); custom shapes
mmap fresh and munmap at death. Pool lock still dropped before any mmap.
No public spawn API change (SpawnOpts is the next commit).
Tests: shape rounding + accessors, wide-guard faults at both ends
(subprocess), Config::stack_reserve permits >64 KiB recursion that
previously could only segfault.
192 lines
5.7 KiB
Rust
192 lines
5.7 KiB
Rust
//! Stack allocator tests.
|
|
//!
|
|
//! Covers allocation, alignment, read/write across the usable region, and
|
|
//! (via subprocess) that the guard page actually SIGSEGVs.
|
|
|
|
use smarm::stack::Stack;
|
|
|
|
#[test]
|
|
fn top_is_16_byte_aligned() {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
assert_eq!(s.top() as usize % 16, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn top_is_within_allocation() {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
let top = s.top() as usize;
|
|
let base = s.usable_base() as usize;
|
|
assert!(top > base);
|
|
assert!(top <= base + s.stack_size());
|
|
}
|
|
|
|
#[test]
|
|
fn write_and_read_top_of_stack() {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
let sentinel: u64 = 0xDEAD_BEEF_CAFE_1234;
|
|
unsafe {
|
|
let ptr = s.top().sub(8) as *mut u64;
|
|
ptr.write_volatile(sentinel);
|
|
assert_eq!(ptr.read_volatile(), sentinel);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn write_and_read_bottom_of_usable_region() {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
let sentinel: u64 = 0x0102_0304_0506_0708;
|
|
unsafe {
|
|
let ptr = s.usable_base() as *mut u64;
|
|
ptr.write_volatile(sentinel);
|
|
assert_eq!(ptr.read_volatile(), sentinel);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn small_stack_allocates() {
|
|
assert!(Stack::new(4096, 4096).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn large_stack_allocates() {
|
|
assert!(Stack::new(8 * 1024 * 1024, 4096).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn stack_size_at_least_requested() {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
assert!(s.stack_size() >= 64 * 1024);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Guard page SIGSEGV tests — subprocess-based.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
use std::env;
|
|
use std::process::Command;
|
|
|
|
fn run_as_child_if_requested() {
|
|
match env::var("SMARM_SUBTEST").as_deref() {
|
|
Ok("guard_page_direct") => {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
unsafe {
|
|
let guard_ptr = s.usable_base().sub(1);
|
|
guard_ptr.write_volatile(0xAB);
|
|
}
|
|
std::process::exit(0);
|
|
}
|
|
Ok("wide_guard_top") => {
|
|
// One byte below the usable region, 64 KiB guard: must fault.
|
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
|
unsafe { s.usable_base().sub(1).write_volatile(0xAB); }
|
|
std::process::exit(0);
|
|
}
|
|
Ok("wide_guard_bottom") => {
|
|
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
|
// leap over a small guard lands here — must still fault.
|
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
|
unsafe { s.usable_base().sub(64 * 1024).write_volatile(0xAB); }
|
|
std::process::exit(0);
|
|
}
|
|
Ok("stack_overflow") => {
|
|
let s = Stack::new(64 * 1024, 4096).unwrap();
|
|
unsafe {
|
|
let mut ptr = s.top().sub(1);
|
|
let stop = s.usable_base().sub(1);
|
|
while ptr >= stop {
|
|
ptr.write_volatile(0xFF);
|
|
ptr = ptr.sub(1);
|
|
}
|
|
}
|
|
std::process::exit(0);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn spawn_subtest(name: &str) -> std::process::ExitStatus {
|
|
let exe = env::current_exe().unwrap();
|
|
Command::new(exe)
|
|
.env("SMARM_SUBTEST", name)
|
|
.args(["--test-threads=1", "--quiet"])
|
|
.status()
|
|
.expect("failed to spawn subprocess")
|
|
}
|
|
|
|
#[test]
|
|
fn guard_page_causes_sigsegv() {
|
|
run_as_child_if_requested();
|
|
let status = spawn_subtest("guard_page_direct");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::process::ExitStatusExt;
|
|
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stack_overflow_causes_sigsegv() {
|
|
run_as_child_if_requested();
|
|
let status = spawn_subtest("stack_overflow");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::process::ExitStatusExt;
|
|
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RFC 019 — explicit shape: rounding, guard accessor, wide-guard coverage.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn sizes_round_up_to_page() {
|
|
let s = Stack::new(64 * 1024 + 1, 4096 + 1).unwrap();
|
|
assert_eq!(s.stack_size() % 4096, 0);
|
|
assert_eq!(s.guard_size() % 4096, 0);
|
|
assert!(s.stack_size() >= 64 * 1024 + 1);
|
|
assert!(s.guard_size() >= 4096 + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn shape_reports_rounded_sizes() {
|
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
|
assert_eq!(s.shape(), (64 * 1024, 64 * 1024));
|
|
}
|
|
|
|
#[test]
|
|
fn usable_base_sits_above_guard() {
|
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
|
// The usable region must start exactly guard_size above the mapping
|
|
// base: a write at usable_base is legal, one byte below is not (the
|
|
// subprocess tests below prove the "not").
|
|
let sentinel: u64 = 0x1111_2222_3333_4444;
|
|
unsafe {
|
|
let ptr = s.usable_base() as *mut u64;
|
|
ptr.write_volatile(sentinel);
|
|
assert_eq!(ptr.read_volatile(), sentinel);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wide_guard_faults_at_top() {
|
|
run_as_child_if_requested();
|
|
let status = spawn_subtest("wide_guard_top");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::process::ExitStatusExt;
|
|
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wide_guard_faults_at_bottom() {
|
|
run_as_child_if_requested();
|
|
let status = spawn_subtest("wide_guard_bottom");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::process::ExitStatusExt;
|
|
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
|
}
|
|
}
|