use core::ptr::NonNull;
use std::io;
use std::ops::Range;
use std::ptr;
#[cfg(asan)]
use std::sync::Mutex;
use crate::prelude::*;
use crate::runtime::vm::{
VMContext, VMFuncRef, VMHostArray, VMPayloads, ValRaw, VmPtr, host_page_size,
};
#[derive(Debug, PartialEq, Eq)]
pub enum Allocator {
Mmap,
Custom,
}
#[cfg(asan)]
static ASAN_STACKS: Mutex<Vec<(usize, usize)>> = Mutex::new(Vec::new());
#[derive(Debug)]
#[repr(C)]
pub struct VMContinuationStack {
top: VmPtr<u8>,
len: usize,
allocator: Allocator,
}
impl VMContinuationStack {
pub fn new(size: usize) -> io::Result<Self> {
let page_size = host_page_size();
let size = if size == 0 {
page_size
} else {
size.next_multiple_of(page_size)
};
unsafe {
let mmap_len = size + page_size;
#[cfg(asan)]
if let Some((base, len)) = {
let mut stacks = ASAN_STACKS.lock().unwrap();
stacks
.iter()
.position(|(_, len)| *len == mmap_len)
.map(|index| stacks.swap_remove(index))
} {
return Ok(Self {
top: VmPtr::from(
NonNull::new((base as *mut u8).add(len))
.expect("a cached continuation stack must have a non-null top"),
),
len,
allocator: Allocator::Mmap,
});
}
let mmap = rustix::mm::mmap_anonymous(
ptr::null_mut(),
mmap_len,
rustix::mm::ProtFlags::empty(),
rustix::mm::MapFlags::PRIVATE,
)?;
rustix::mm::mprotect(
mmap.cast::<u8>().add(page_size).cast(),
size,
rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE,
)?;
Ok(Self {
top: VmPtr::from(NonNull::new_unchecked(mmap.cast::<u8>().add(mmap_len))),
len: mmap_len,
allocator: Allocator::Mmap,
})
}
}
pub fn unallocated() -> Self {
Self {
top: VmPtr::dangling(),
len: 0,
allocator: Allocator::Custom,
}
}
pub fn is_unallocated(&self) -> bool {
self.len == 0
}
pub unsafe fn from_raw_parts(
base: *mut u8,
_guard_size: usize,
len: usize,
) -> io::Result<Self> {
Ok(Self {
top: VmPtr::from(
NonNull::new(unsafe { base.add(len) })
.expect("a continuation stack's top-of-stack pointer must be non-null"),
),
len,
allocator: Allocator::Custom,
})
}
pub fn is_from_raw_parts(&self) -> bool {
self.allocator == Allocator::Custom
}
pub fn top(&self) -> Option<*mut u8> {
Some(self.top.as_ptr())
}
pub fn range(&self) -> Option<Range<usize>> {
let top = self.top.as_ptr();
let base = unsafe { top.sub(self.len).addr() };
Some(base..base + self.len)
}
#[cfg(asan)]
pub fn asan_range(&self) -> Option<Range<usize>> {
let top = self.top.addr().get();
let bottom = top - self.len;
let usable_bottom = match self.allocator {
Allocator::Mmap =>
{
bottom + host_page_size()
}
Allocator::Custom => bottom,
};
Some(usable_bottom..top)
}
pub fn control_context_instruction_pointer(&self) -> usize {
unsafe {
let ptr = self.top.as_ptr().sub(8).cast::<usize>();
*ptr
}
}
pub fn control_context_frame_pointer(&self) -> usize {
unsafe {
let ptr = self.top.as_ptr().sub(16).cast::<usize>();
*ptr
}
}
pub fn control_context_stack_pointer(&self) -> usize {
unsafe {
let ptr = self.top.as_ptr().sub(24).cast::<usize>();
*ptr
}
}
pub fn initialize(
&self,
func_ref: *const VMFuncRef,
caller_vmctx: *mut VMContext,
args: *mut VMPayloads,
parameter_count: u32,
return_value_count: u32,
gc_refs: bool,
) -> Result<()> {
let tos = self.top.as_ptr();
unsafe {
let store = |tos_neg_offset, value| {
let target = tos.sub(tos_neg_offset).cast::<usize>();
target.write(value)
};
let payloads = &mut *args;
let args_ref = &mut payloads.buffer;
let args_capacity = std::cmp::max(parameter_count, return_value_count);
debug_assert_eq!(args_ref.capacity, 0);
debug_assert_eq!(args_ref.length, 0);
let args_data_size = usize::try_from(args_capacity)?
.checked_mul(std::mem::size_of::<ValRaw>())
.ok_or_else(|| {
format_err!(
"continuation function type with {args_capacity} args \
overflows stack control data size calculation"
)
})?;
let gc_refs_data_size = if cfg!(feature = "gc") && gc_refs {
usize::try_from(args_capacity)?
.checked_add(15)
.map(|s| s & !15)
.ok_or_else(|| {
format_err!(
"continuation function type with {args_capacity} args \
overflows stack control data size calculation"
)
})?
} else {
0
};
let dynamic_data_size =
args_data_size
.checked_add(gc_refs_data_size)
.ok_or_else(|| {
format_err!(
"continuation function type with {args_capacity} args \
overflows stack control data size calculation"
)
})?;
let total_control_size = dynamic_data_size.checked_add(0x40).ok_or_else(|| {
format_err!(
"continuation function type with {args_capacity} args \
overflows stack control data size calculation"
)
})?;
let page_size = host_page_size();
let usable_len = match self.allocator {
Allocator::Mmap => self.len.saturating_sub(page_size),
Allocator::Custom => self.len,
};
ensure!(
total_control_size <= usable_len,
"continuation function type requires {total_control_size} bytes \
of stack control data, which exceeds the {usable_len}-byte \
usable stack allocation",
);
let args_data_ptr = if args_capacity == 0 {
ptr::null_mut()
} else {
tos.sub(0x20 + args_data_size)
};
args_ref.capacity = args_capacity;
args_ref.data = NonNull::new(args_data_ptr).map(VmPtr::from);
if cfg!(feature = "gc") && gc_refs {
let data = if args_capacity == 0 {
ptr::null_mut()
} else {
tos.sub(0x20 + dynamic_data_size)
};
if args_capacity > 0 {
data.write_bytes(0, usize::try_from(args_capacity)?);
}
payloads.gc_ref_data = NonNull::new(data).map(VmPtr::from);
}
let to_store = [
(0x08, wasmtime_continuation_start_address().addr()),
(0x10, tos.sub(0x10).addr()),
(0x18, tos.sub(total_control_size).addr()),
(0x20, usize::try_from(args_capacity)?),
(0x28 + dynamic_data_size, func_ref.addr()),
(0x30 + dynamic_data_size, caller_vmctx.addr()),
(
0x38 + dynamic_data_size,
(args_ref as *mut VMHostArray).addr(),
),
(
0x40 + dynamic_data_size,
usize::try_from(return_value_count)?,
),
];
for (offset, data) in to_store {
store(offset, data);
}
}
Ok(())
}
}
impl Drop for VMContinuationStack {
#[cfg(asan)]
fn drop(&mut self) {
unsafe {
match self.allocator {
Allocator::Mmap => {
let bottom = self.top.as_ptr().sub(self.len);
ASAN_STACKS.lock().unwrap().push((bottom.addr(), self.len))
}
Allocator::Custom => {} }
}
}
#[cfg(not(asan))]
fn drop(&mut self) {
unsafe {
match self.allocator {
Allocator::Mmap => {
let bottom = self.top.as_ptr().sub(self.len);
let ret = rustix::mm::munmap(bottom as _, self.len);
debug_assert!(ret.is_ok())
}
Allocator::Custom => {} }
}
}
}
unsafe extern "C" fn fiber_start(
func_ref: *mut VMFuncRef,
caller_vmctx: *mut VMContext,
args: *mut VMHostArray,
return_value_count: u32,
) -> bool {
unsafe {
#[cfg(asan)]
crate::vm::stack_switching::asan::fiber_start_complete(args);
let func_ref = NonNull::new(func_ref).unwrap();
let caller_vmxtx = NonNull::new_unchecked(caller_vmctx);
let args = &mut *args;
let params_and_returns: NonNull<[ValRaw]> = if args.capacity == 0 {
NonNull::from(&[])
} else {
let data = args
.data
.expect("non-empty continuation arguments require an allocated buffer")
.as_ptr();
std::slice::from_raw_parts_mut(
data.cast::<ValRaw>(),
usize::try_from(args.capacity).unwrap(),
)
.into()
};
let succeeded = VMFuncRef::array_call(func_ref, None, caller_vmxtx, params_and_returns);
if succeeded {
args.length = return_value_count;
}
succeeded
}
}
cfg_select! {
target_arch = "x86_64" => {
mod x86_64;
use x86_64::*;
}
_ => {
compile_error!("the stack switching feature is not supported on this CPU architecture");
}
}