1 //! The stack layout is expected to look like so: 2 //! 3 //! 4 //! ```text 5 //! 0xB000 +-----------------------+ <- top of stack (TOS) 6 //! | saved RIP | 7 //! 0xAff8 +-----------------------+ 8 //! | saved RBP | 9 //! 0xAff0 +-----------------------+ 10 //! | saved RSP | 11 //! 0xAfe8 +-----------------------+ <- beginning of "control context", 12 //! | args_capacity | 13 //! 0xAfe0 +-----------------------+ 14 //! | args buffer, size: | 15 //! | (16 * args_capacity) | 16 //! 0xAfc0 +-----------------------+ <- below: beginning of usable stack space 17 //! | | (16-byte aligned) 18 //! | | 19 //! ~ ... ~ <- actual native stack space to use 20 //! | | 21 //! 0x1000 +-----------------------+ 22 //! | guard page | <- (not currently enabled) 23 //! 0x0000 +-----------------------+ 24 //! ``` 25 //! 26 //! The "control context" indicates how to resume a computation. The layout is 27 //! determined by Cranelift's stack_switch instruction, which reads and writes 28 //! these fields. The fields are used as follows, where we distinguish two 29 //! cases: 30 //! 31 //! 1. 32 //! If the continuation is currently active (i.e., running directly, or ancestor 33 //! of the running continuation), it stores the PC, RSP, and RBP of the *parent* 34 //! of the running continuation. 35 //! 36 //! 2. 37 //! If the picture shows a suspended computation, the fields store the PC, RSP, 38 //! and RBP at the time of the suspension. 39 //! 40 //! Note that this design ensures that external tools can construct backtraces 41 //! in the presence of stack switching by using frame pointers only: The 42 //! wasmtime_continuation_start trampoline uses the address of the RBP field in the 43 //! control context (0xAff0 above) as its frame pointer. This means that when 44 //! passing the wasmtime_continuation_start frame while doing frame pointer walking, 45 //! the parent of that frame is the last frame in the parent of this 46 //! continuation. 47 //! 48 //! Wasmtime's own mechanism for constructing backtraces also relies on frame 49 //! pointer chains. However, it understands continuations and does not rely on 50 //! the trickery outlined here to go from the frames in one continuation to the 51 //! parent. 52 //! 53 //! The args buffer is used as follows: It is used by the array calling 54 //! trampoline to read and store the arguments and return values of the function 55 //! running inside the continuation. If this function has m parameters and n 56 //! return values, then args_capacity is defined as max(m, n) and the size of 57 //! the args buffer is args_capacity * 16 bytes. The start address (0xAfc0 in 58 //! the example above, thus assuming args_capacity = 2) is saved as the `data` 59 //! field of the VMContRef's `args` object. 60 61 #![allow(unused_macros)] 62 63 use core::ptr::NonNull; 64 use std::io; 65 use std::ops::Range; 66 use std::ptr; 67 68 use crate::runtime::vm::stack_switching::VMHostArray; 69 use crate::runtime::vm::{VMContext, VMFuncRef, ValRaw}; 70 71 #[derive(Debug, PartialEq, Eq)] 72 pub enum Allocator { 73 Mmap, 74 Custom, 75 } 76 77 #[derive(Debug)] 78 #[repr(C)] 79 pub struct VMContinuationStack { 80 // The top of the stack; for stacks allocated by the fiber implementation itself, 81 // the base address of the allocation will be `top.sub(len.unwrap())` 82 top: *mut u8, 83 // The length of the stack 84 len: usize, 85 // allocation strategy 86 allocator: Allocator, 87 } 88 89 impl VMContinuationStack { 90 pub fn new(size: usize) -> io::Result<Self> { 91 // Round up our stack size request to the nearest multiple of the 92 // page size. 93 let page_size = rustix::param::page_size(); 94 let size = if size == 0 { 95 page_size 96 } else { 97 size.next_multiple_of(page_size) 98 }; 99 100 unsafe { 101 // Add in one page for a guard page and then ask for some memory. 102 let mmap_len = size + page_size; 103 let mmap = rustix::mm::mmap_anonymous( 104 ptr::null_mut(), 105 mmap_len, 106 rustix::mm::ProtFlags::empty(), 107 rustix::mm::MapFlags::PRIVATE, 108 )?; 109 110 rustix::mm::mprotect( 111 mmap.cast::<u8>().add(page_size).cast(), 112 size, 113 rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE, 114 )?; 115 116 Ok(Self { 117 top: mmap.cast::<u8>().add(mmap_len), 118 len: mmap_len, 119 allocator: Allocator::Mmap, 120 }) 121 } 122 } 123 124 pub fn unallocated() -> Self { 125 Self { 126 top: std::ptr::null_mut(), 127 len: 0, 128 allocator: Allocator::Custom, 129 } 130 } 131 132 pub fn is_unallocated(&self) -> bool { 133 debug_assert_eq!(self.len == 0, self.top == std::ptr::null_mut()); 134 self.len == 0 135 } 136 137 #[allow(clippy::missing_safety_doc)] 138 pub unsafe fn from_raw_parts( 139 base: *mut u8, 140 _guard_size: usize, 141 len: usize, 142 ) -> io::Result<Self> { 143 Ok(Self { 144 top: base.add(len), 145 len, 146 allocator: Allocator::Custom, 147 }) 148 } 149 150 pub fn is_from_raw_parts(&self) -> bool { 151 self.allocator == Allocator::Custom 152 } 153 154 pub fn top(&self) -> Option<*mut u8> { 155 Some(self.top) 156 } 157 158 pub fn range(&self) -> Option<Range<usize>> { 159 let base = unsafe { self.top.sub(self.len).addr() }; 160 Some(base..base + self.len) 161 } 162 163 pub fn control_context_instruction_pointer(&self) -> usize { 164 // See picture at top of this file: 165 // RIP is stored 8 bytes below top of stack. 166 unsafe { 167 let ptr = self.top.sub(8).cast::<usize>(); 168 *ptr 169 } 170 } 171 172 pub fn control_context_frame_pointer(&self) -> usize { 173 // See picture at top of this file: 174 // RBP is stored 16 bytes below top of stack. 175 unsafe { 176 let ptr = self.top.sub(16).cast::<usize>(); 177 *ptr 178 } 179 } 180 181 pub fn control_context_stack_pointer(&self) -> usize { 182 // See picture at top of this file: 183 // RSP is stored 24 bytes below top of stack. 184 unsafe { 185 let ptr = self.top.sub(24).cast::<usize>(); 186 *ptr 187 } 188 } 189 190 /// This function installs the launchpad for the computation to run on the 191 /// fiber, such that executing a `stack_switch` instruction on the stack 192 /// actually runs the desired computation. 193 /// 194 /// Concretely, switching to the stack prepared by this function 195 /// causes that we enter `wasmtime_continuation_start`, which then in turn 196 /// calls `fiber_start` with the following arguments: 197 /// TOS, func_ref, caller_vmctx, args_ptr, args_capacity 198 /// 199 /// Note that at this point we also allocate the args buffer 200 /// (see picture at the top of this file). 201 /// We define `args_capacity` as the max of parameter and return value count. 202 /// Then the size s of the actual buffer size is calculated as follows: 203 /// s = size_of(ValRaw) * `args_capacity`, 204 /// 205 /// Note that this value is used below, and we may have s = 0. 206 /// 207 /// The layout of the VMContinuationStack near the top of stack (TOS) 208 /// *after* running this function is as follows: 209 /// 210 /// 211 /// Offset from | 212 /// TOS | Contents 213 /// ---------------|------------------------------------------------------- 214 /// -0x08 | address of wasmtime_continuation_start function (future PC) 215 /// -0x10 | TOS - 0x10 (future RBP) 216 /// -0x18 | TOS - 0x40 - s (future RSP) 217 /// -0x20 | args_capacity 218 /// 219 /// 220 /// The data stored behind the args buffer is as follows: 221 /// 222 /// Offset from | 223 /// TOS | Contents 224 /// ---------------|------------------------------------------------------- 225 /// -0x28 - s | func_ref 226 /// -0x30 - s | caller_vmctx 227 /// -0x38 - s | args (of type *mut ArrayRef<ValRaw>) 228 /// -0x40 - s | return_value_count 229 pub fn initialize( 230 &self, 231 func_ref: *const VMFuncRef, 232 caller_vmctx: *mut VMContext, 233 args: *mut VMHostArray<ValRaw>, 234 parameter_count: u32, 235 return_value_count: u32, 236 ) { 237 let tos = self.top; 238 239 unsafe { 240 let store = |tos_neg_offset, value| { 241 let target = tos.sub(tos_neg_offset).cast::<usize>(); 242 target.write(value) 243 }; 244 245 let args_ref = &mut *args; 246 let args_capacity = std::cmp::max(parameter_count, return_value_count); 247 // The args object must currently be empty. 248 debug_assert_eq!(args_ref.capacity, 0); 249 debug_assert_eq!(args_ref.length, 0); 250 251 let args_data_size = 252 usize::try_from(args_capacity).unwrap() * std::mem::size_of::<ValRaw>(); 253 let args_data_ptr = if args_capacity == 0 { 254 ptr::null_mut() 255 } else { 256 tos.sub(0x20 + args_data_size) 257 }; 258 259 args_ref.capacity = args_capacity; 260 args_ref.data = args_data_ptr.cast::<ValRaw>(); 261 262 let to_store = [ 263 // Data near top of stack: 264 (0x08, wasmtime_continuation_start as usize), 265 (0x10, tos.sub(0x10).addr()), 266 (0x18, tos.sub(0x40 + args_data_size).addr()), 267 (0x20, usize::try_from(args_capacity).unwrap()), 268 // Data after the args buffer: 269 (0x28 + args_data_size, func_ref.addr()), 270 (0x30 + args_data_size, caller_vmctx.addr()), 271 (0x38 + args_data_size, args.addr()), 272 ( 273 0x40 + args_data_size, 274 usize::try_from(return_value_count).unwrap(), 275 ), 276 ]; 277 278 for (offset, data) in to_store { 279 store(offset, data); 280 } 281 } 282 } 283 } 284 285 impl Drop for VMContinuationStack { 286 fn drop(&mut self) { 287 unsafe { 288 match self.allocator { 289 Allocator::Mmap => { 290 let ret = rustix::mm::munmap(self.top.sub(self.len) as _, self.len); 291 debug_assert!(ret.is_ok()); 292 } 293 Allocator::Custom => {} // It's the creator's responsibility to reclaim the memory. 294 } 295 } 296 } 297 } 298 299 unsafe extern "C" { 300 #[allow(dead_code)] // only used in inline assembly for some platforms 301 fn wasmtime_continuation_start(); 302 } 303 304 /// This function is responsible for actually running a wasm function inside a 305 /// continuation. It is only ever called from `wasmtime_continuation_start`. 306 unsafe extern "C" fn fiber_start( 307 func_ref: *mut VMFuncRef, 308 caller_vmctx: *mut VMContext, 309 args: *mut VMHostArray<ValRaw>, 310 return_value_count: u32, 311 ) { 312 unsafe { 313 let func_ref = NonNull::new(func_ref).unwrap(); 314 let caller_vmxtx = NonNull::new_unchecked(caller_vmctx); 315 let args = &mut *args; 316 let params_and_returns: NonNull<[ValRaw]> = if args.capacity == 0 { 317 NonNull::from(&[]) 318 } else { 319 std::slice::from_raw_parts_mut(args.data, usize::try_from(args.capacity).unwrap()) 320 .into() 321 }; 322 323 // NOTE(frank-emrich) The usage of the `caller_vmctx` is probably not 324 // 100% correct here. Currently, we determine the "caller" vmctx when 325 // initializing the fiber stack/continuation (i.e. as part of 326 // `cont.new`). However, we may subsequenly `resume` the continuation 327 // from a different Wasm instance. The way to fix this would be to make 328 // the currently active `VMContext` an additional parameter of 329 // `wasmtime_continuation_switch` and pipe it through to this point. However, 330 // since the caller vmctx is only really used to access stuff in the 331 // underlying `Store`, it's fine to be slightly sloppy about the exact 332 // value we set. 333 // 334 // TODO(dhil): we are ignoring the boolean return value 335 // here... we probably shouldn't. 336 VMFuncRef::array_call(func_ref, None, caller_vmxtx, params_and_returns); 337 338 // The array call trampoline should have just written 339 // `return_value_count` values to the `args` buffer. Let's reflect that 340 // in its length field, to make various bounds checks happy. 341 args.length = return_value_count; 342 343 // Note that after this function returns, wasmtime_continuation_start 344 // will switch back to the parent stack. 345 } 346 } 347 348 cfg_if::cfg_if! { 349 if #[cfg(target_arch = "x86_64")] { 350 mod x86_64; 351 } else { 352 // Note that this should be unreachable: In stack.rs, we currently select 353 // the module defined in the current file only if we are on unix AND 354 // x86_64. 355 compile_error!("the stack switching feature is not supported on this CPU architecture"); 356 } 357 } 358