154 lines
4.9 KiB
Rust
154 lines
4.9 KiB
Rust
//! 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}"
|
|
);
|
|
}
|