//! RFC 019 §7 — overflow diagnostics, observed from outside via subprocess //! (mirrors tests/stack.rs's harness, plus stderr capture). //! //! Four cases: //! - Rust recursion at defaults: probed frames walk into the guard → //! tier-1 definitive message, death by SIGSEGV. //! - FFI canary (96 KiB unprobed C local) at defaults: first touch lands //! inside the 1 MiB guard → tier-1 message. //! - FFI canary with the guard shrunk to 4 KiB: the frame steps over it //! into unmapped VA below → tier-2 "stepped over" message. This is the //! RFC's motivating incident (cargo-vendored gz build) reproduced. //! - FFI canary with reserve raised to 256 KiB: fits, runs clean, exits 0 — //! the §1 knob is the fix, proven by the same frame. use std::env; use std::process::Command; unsafe extern "C" { fn smarm_canary_burn(); } /// Unbounded probed recursion; each frame dirties 4 KiB. black_box defeats /// tail-call elision so the walk is real. #[inline(never)] #[allow(unconditional_recursion)] fn recurse_forever(depth: u64) -> u64 { let mut local = [0u8; 4096]; local[0] = depth as u8; std::hint::black_box(&mut local); recurse_forever(depth + 1).wrapping_add(local[0] as u64) } fn run_as_child_if_requested() { let mode = match env::var("SMARM_DIAG_SUBTEST") { Ok(m) => m, Err(_) => return, }; use smarm::runtime::Config; use smarm::{spawn_with, SpawnOpts}; let rt = smarm::runtime::init(Config::exact(1)); rt.run(move || { let opts = match mode.as_str() { "rust_overflow" | "ffi_tier1" => SpawnOpts::default(), // Small guard: the canary's 96 KiB displacement clears it. "ffi_tier2" => SpawnOpts { guard_size: Some(4096), ..SpawnOpts::default() }, // Enough reserve: the same frame simply fits. "ffi_clean" => SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() }, other => panic!("unknown subtest {other}"), }; let is_rust = mode == "rust_overflow"; spawn_with(opts, move || { if is_rust { std::hint::black_box(recurse_forever(0)); } else { unsafe { smarm_canary_burn() }; } }) .join() .unwrap(); }); std::process::exit(0); } fn spawn_subtest(name: &str) -> std::process::Output { let exe = env::current_exe().unwrap(); Command::new(exe) .env("SMARM_DIAG_SUBTEST", name) .args(["--test-threads=1", "--quiet"]) .output() .expect("failed to spawn subprocess") } #[cfg(unix)] fn assert_died_sigsegv(out: &std::process::Output) { use std::os::unix::process::ExitStatusExt; assert_eq!( out.status.signal(), Some(11), "expected death by SIGSEGV, got {:?}; stderr:\n{}", out.status, String::from_utf8_lossy(&out.stderr) ); } #[test] fn rust_overflow_dies_with_tier1_message() { run_as_child_if_requested(); let out = spawn_subtest("rust_overflow"); assert_died_sigsegv(&out); let err = String::from_utf8_lossy(&out.stderr); assert!( err.contains("overflowed its stack") && err.contains("in the guard region"), "missing tier-1 diagnostic; stderr:\n{err}" ); assert!(err.contains("reserve=65536"), "wrong reserve in message:\n{err}"); assert!(err.contains("guard=1048576"), "wrong guard in message:\n{err}"); } #[test] fn ffi_canary_at_defaults_dies_with_tier1_message() { run_as_child_if_requested(); let out = spawn_subtest("ffi_tier1"); assert_died_sigsegv(&out); let err = String::from_utf8_lossy(&out.stderr); // 96 KiB displacement from a 64 KiB reserve lands ~32 KiB into the // 1 MiB guard: definitively classified. assert!( err.contains("in the guard region"), "wide guard should catch the unprobed frame in tier 1; stderr:\n{err}" ); } #[test] fn ffi_canary_over_small_guard_dies_with_tier2_message() { run_as_child_if_requested(); let out = spawn_subtest("ffi_tier2"); assert_died_sigsegv(&out); let err = String::from_utf8_lossy(&out.stderr); assert!( err.contains("stepped over it") && err.contains("below the guard"), "expected tier-2 overshoot attribution; stderr:\n{err}" ); assert!(err.contains("guard=4096"), "wrong guard in message:\n{err}"); } #[test] fn ffi_canary_with_enough_reserve_runs_clean() { run_as_child_if_requested(); let out = spawn_subtest("ffi_clean"); assert!( out.status.success(), "canary should fit in 256 KiB reserve, got {:?}; stderr:\n{}", out.status, String::from_utf8_lossy(&out.stderr) ); let err = String::from_utf8_lossy(&out.stderr); assert!( !err.contains("smarm: actor"), "no diagnostic expected on the clean path; stderr:\n{err}" ); }