use libtest_mimic::{Arguments, Trial};
use std::env;
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::process::{Command, ExitStatus};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use wasmtime::*;
const VAR_NAME: &str = "__TEST_TO_RUN";
const CONFIRM: &str = "well at least we ran up to the crash";
fn segfault() -> ! {
unsafe {
println!("{CONFIRM}");
io::stdout().flush().unwrap();
*(0x4 as *mut i32) = 3;
unreachable!()
}
}
fn allocate_stack_space() -> ! {
let _a = [0u8; 1024];
for _ in 0..100000 {
allocate_stack_space();
}
unreachable!()
}
fn overrun_the_stack() -> ! {
println!("{CONFIRM}");
io::stdout().flush().unwrap();
allocate_stack_space();
}
fn run_future<F: Future>(future: F) -> F::Output {
let mut f = Pin::from(Box::new(future));
let waker = dummy_waker();
let mut cx = Context::from_waker(&waker);
loop {
match f.as_mut().poll(&mut cx) {
Poll::Ready(val) => break val,
Poll::Pending => {}
}
}
}
fn dummy_waker() -> Waker {
return unsafe { Waker::from_raw(clone(5 as *const _)) };
unsafe fn clone(ptr: *const ()) -> RawWaker {
assert_eq!(ptr as usize, 5);
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(ptr, &VTABLE)
}
unsafe fn wake(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
unsafe fn wake_by_ref(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
unsafe fn drop(ptr: *const ()) {
assert_eq!(ptr as usize, 5);
}
}
#[derive(PartialEq, Copy, Clone)]
enum StackOverflow {
No,
Host,
HostAsyncStack,
Wasm,
}
fn main() {
if cfg!(miri) || cfg!(asan) {
return;
}
if wasmtime_test_util::cargo_test_runner().is_some() {
return;
}
#[cfg(unix)]
unsafe {
let zero = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let rc = libc::setrlimit(libc::RLIMIT_CORE, &zero);
assert_eq!(
rc,
0,
"failed to disable core dumps: {}",
std::io::Error::last_os_error()
);
}
let tests: &[(&str, fn(), StackOverflow)] = &[
("normal segfault", || segfault(), StackOverflow::No),
(
"make instance then segfault",
|| {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, "(module)").unwrap();
let _instance = Instance::new(&mut store, &module, &[]).unwrap();
segfault();
},
StackOverflow::No,
),
(
"make instance then overrun the stack",
|| {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, "(module)").unwrap();
let _instance = Instance::new(&mut store, &module, &[]).unwrap();
overrun_the_stack();
},
StackOverflow::Host,
),
(
"segfault in a host function",
|| {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, r#"(import "" "" (func)) (start 0)"#).unwrap();
let segfault = Func::wrap(&mut store, || -> () { segfault() });
Instance::new(&mut store, &module, &[segfault.into()]).unwrap();
unreachable!();
},
StackOverflow::No,
),
(
"hit async stack guard page",
|| {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let f = Func::wrap_async(&mut store, |_, _: ()| {
Box::new(async {
if true {
overrun_the_stack();
}
})
});
run_future(f.call_async(&mut store, &[], &mut [])).unwrap();
unreachable!();
},
StackOverflow::HostAsyncStack,
),
(
"overrun 8k with misconfigured host",
|| overrun_with_big_module(8 << 10),
StackOverflow::Wasm,
),
(
"overrun 32k with misconfigured host",
|| overrun_with_big_module(32 << 10),
StackOverflow::Wasm,
),
#[cfg(not(any(target_arch = "riscv64")))]
(
"hit async stack guard page with pooling allocator",
|| {
let mut config = Config::default();
let mut cfg = PoolingAllocationConfig::default();
cfg.total_memories(1);
cfg.max_memory_size(1 << 16);
cfg.total_tables(1);
cfg.table_elements(10);
cfg.total_stacks(1);
config.allocation_strategy(cfg);
let engine = Engine::new(&config).unwrap();
let mut store = Store::new(&engine, ());
let f = Func::wrap_async(&mut store, |_, _: ()| {
Box::new(async {
if true {
overrun_the_stack();
}
})
});
run_future(f.call_async(&mut store, &[], &mut [])).unwrap();
unreachable!();
},
StackOverflow::HostAsyncStack,
),
];
match env::var(VAR_NAME) {
Ok(s) => {
let test = tests
.iter()
.find(|p| p.0 == s)
.expect("failed to find test")
.1;
test();
}
Err(_) => {
let mut trials = Vec::new();
for (name, _test, stack_overflow) in tests {
trials.push(Trial::test(name.to_string(), || {
run_test(name, *stack_overflow);
Ok(())
}));
}
libtest_mimic::run(&Arguments::from_args(), trials).exit()
}
}
}
fn run_test(name: &str, stack_overflow: StackOverflow) {
let me = env::current_exe().unwrap();
let mut cmd = Command::new(me);
cmd.env(VAR_NAME, name);
let output = cmd.output().expect("failed to spawn subprocess");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut desc = format!("got status: {}", output.status);
if !stdout.trim().is_empty() {
desc.push_str("\nstdout: ----\n");
desc.push_str(" ");
desc.push_str(&stdout.replace("\n", "\n "));
}
if !stderr.trim().is_empty() {
desc.push_str("\nstderr: ----\n");
desc.push_str(" ");
desc.push_str(&stderr.replace("\n", "\n "));
}
match stack_overflow {
StackOverflow::Host | StackOverflow::Wasm => {
let native_stack_overflow = is_stack_overflow(&output.status, &stderr);
let expect_native_overflow =
stack_overflow == StackOverflow::Host || cranelift_native::builder().is_ok();
if native_stack_overflow == expect_native_overflow {
assert!(
stdout.trim().ends_with(CONFIRM),
"failed to find confirmation in test `{name}`\n{desc}"
);
} else {
panic!("\n\nexpected a stack overflow on `{name}`\n{desc}\n\n");
}
}
StackOverflow::HostAsyncStack => {
let native_stack_overflow = if cfg!(has_native_signals) {
is_stack_overflow(&output.status, &stderr)
} else {
is_segfault(&output.status)
};
assert!(
native_stack_overflow,
"expected a native stack overflow for `{name}`:\n{desc}"
);
assert!(
stdout.trim().ends_with(CONFIRM),
"failed to find confirmation in test `{name}`\n{desc}"
);
}
StackOverflow::No => {
if is_segfault(&output.status) {
assert!(
stdout.trim().ends_with(CONFIRM) && stderr.is_empty(),
"failed to find confirmation in test `{name}`\n{desc}"
);
} else {
panic!("\n\nexpected a segfault on `{name}`\n{desc}\n\n");
}
}
}
}
#[cfg(unix)]
fn is_segfault(status: &ExitStatus) -> bool {
use std::os::unix::prelude::*;
match status.signal() {
Some(libc::SIGSEGV) => true,
_ => false,
}
}
#[cfg(unix)]
fn is_stack_overflow(status: &ExitStatus, stderr: &str) -> bool {
use std::os::unix::prelude::*;
match status.signal() {
Some(libc::SIGABRT) => {}
_ => return false,
}
stderr.contains("has overflowed its stack")
}
#[cfg(windows)]
fn is_segfault(status: &ExitStatus) -> bool {
match status.code().map(|s| s as u32) {
Some(0xc0000005) => true,
_ => false,
}
}
#[cfg(windows)]
fn is_stack_overflow(status: &ExitStatus, _stderr: &str) -> bool {
match status.code().map(|s| s as u32) {
Some(0xc00000fd) => true,
_ => false,
}
}
fn overrun_with_big_module(approx_stack: usize) {
let n = approx_stack / 10 / 8;
let mut s = String::new();
s.push_str("(module\n");
s.push_str("(func $big_stack\n");
for _ in 0..n {
s.push_str("call $get\n");
}
for _ in 0..n {
s.push_str("call $take\n");
}
s.push_str(")\n");
s.push_str("(func $get (result i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) call $big_stack unreachable)\n");
s.push_str("(func $take (param i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) unreachable)\n");
s.push_str("(func (export \"\") call $big_stack)\n");
s.push_str(")\n");
let mut config = Config::default();
config.max_wasm_stack(100 << 20).async_stack_size(100 << 20);
let engine = Engine::new(&config).unwrap();
let module = Module::new(&engine, &s).unwrap();
let mut store = Store::new(&engine, ());
let i = Instance::new(&mut store, &module, &[]).unwrap();
let f = i.get_typed_func::<(), ()>(&mut store, "").unwrap();
std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(move || {
println!("{CONFIRM}");
f.call(&mut store, ()).unwrap();
})
.unwrap()
.join()
.unwrap();
unreachable!();
}