1 //! The unix fiber implementation has some platform-specific details
2 //! (naturally) but there's a few details of the stack layout which are common
3 //! amongst all platforms using this file. Remember that none of this applies to
4 //! Windows, which is entirely separate.
5 //!
6 //! The stack is expected to look pretty standard with a guard page at the end.
7 //! Currently allocation happens in this file but this is probably going to be
8 //! refactored to happen somewhere else. Otherwise though the stack layout is
9 //! expected to look like so:
10 //!
11 //!
12 //! ```text
13 //! 0xB000 +-----------------------+ <- top of stack
14 //! | &Cell<RunResult> | <- where to store results
15 //! 0xAff8 +-----------------------+
16 //! | *const u8 | <- last sp to resume from
17 //! 0xAff0 +-----------------------+ <- 16-byte aligned
18 //! | |
19 //! ~ ... ~ <- actual native stack space to use
20 //! | |
21 //! 0x1000 +-----------------------+
22 //! | guard page |
23 //! 0x0000 +-----------------------+
24 //! ```
25 //!
26 //! Here `0xAff8` is filled in temporarily while `resume` is running. The fiber
27 //! started with 0xB000 as a parameter so it knows how to find this.
28 //! Additionally `resumes` stores state at 0xAff0 to restart execution, and
29 //! `suspend`, which has 0xB000 so it can find this, will read that and write
30 //! its own resumption information into this slot as well.
31
32 use crate::stackswitch::*;
33 use crate::{RunResult, RuntimeFiberStack};
34 use std::boxed::Box;
35 use std::cell::Cell;
36 use std::ops::Range;
37 use std::ptr;
38 use std::sync::atomic::{AtomicUsize, Ordering};
39 use wasmtime_environ::error::{OutOfMemory, Result};
40 use wasmtime_environ::prelude::*;
41
42 pub type Error = wasmtime_environ::error::Error;
43
44 pub struct FiberStack {
45 base: BasePtr,
46 len: usize,
47
48 /// Stored here to ensure that when this `FiberStack` the backing storage,
49 /// if any, is additionally dropped.
50 storage: FiberStackStorage,
51 }
52
53 struct BasePtr(*mut u8);
54
55 unsafe impl Send for BasePtr {}
56 unsafe impl Sync for BasePtr {}
57
58 enum FiberStackStorage {
59 Mmap(MmapFiberStack),
60 Unmanaged(usize),
61 Custom(Box<dyn RuntimeFiberStack>),
62 }
63
64 // FIXME: this is a duplicate copy of what's already in the `wasmtime` crate. If
65 // this changes that should change over there, and ideally one day we should
66 // probably deduplicate the two.
host_page_size() -> usize67 fn host_page_size() -> usize {
68 static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0);
69
70 return match PAGE_SIZE.load(Ordering::Relaxed) {
71 0 => {
72 let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap() };
73 assert!(size != 0);
74 PAGE_SIZE.store(size, Ordering::Relaxed);
75 size
76 }
77 n => n,
78 };
79 }
80
81 impl FiberStack {
new(size: usize, zeroed: bool) -> Result<Self>82 pub fn new(size: usize, zeroed: bool) -> Result<Self> {
83 let page_size = host_page_size();
84 // The anonymous `mmap`s we use for `FiberStackStorage` are always
85 // zeroed.
86 let _ = zeroed;
87
88 // See comments in `mod asan` below for why asan has a different stack
89 // allocation strategy.
90 if cfg!(asan) {
91 return Self::from_custom(asan::new_fiber_stack(size)?);
92 }
93 let stack = MmapFiberStack::new(size)?;
94
95 // An `MmapFiberStack` allocates a guard page at the bottom of the
96 // region so the base and length of our stack are both offset by a
97 // single page.
98 Ok(FiberStack {
99 base: BasePtr(stack.mapping_base.wrapping_byte_add(page_size)),
100 len: stack.mapping_len - page_size,
101 storage: FiberStackStorage::Mmap(stack),
102 })
103 }
104
from_raw_parts(base: *mut u8, guard_size: usize, len: usize) -> Result<Self>105 pub unsafe fn from_raw_parts(base: *mut u8, guard_size: usize, len: usize) -> Result<Self> {
106 // See comments in `mod asan` below for why asan has a different stack
107 // allocation strategy.
108 if cfg!(asan) {
109 return Self::from_custom(asan::new_fiber_stack(len)?);
110 }
111 Ok(FiberStack {
112 base: BasePtr(unsafe { base.add(guard_size) }),
113 len,
114 storage: FiberStackStorage::Unmanaged(guard_size),
115 })
116 }
117
is_from_raw_parts(&self) -> bool118 pub fn is_from_raw_parts(&self) -> bool {
119 matches!(self.storage, FiberStackStorage::Unmanaged(_))
120 }
121
from_custom(custom: Box<dyn RuntimeFiberStack>) -> Result<Self>122 pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> Result<Self> {
123 let range = custom.range();
124 let page_size = host_page_size();
125 let start_ptr = range.start as *mut u8;
126 assert!(
127 start_ptr.align_offset(page_size) == 0,
128 "expected fiber stack base ({start_ptr:?}) to be page aligned ({page_size:#x})",
129 );
130 let end_ptr = range.end as *const u8;
131 assert!(
132 end_ptr.align_offset(page_size) == 0,
133 "expected fiber stack end ({end_ptr:?}) to be page aligned ({page_size:#x})",
134 );
135 Ok(FiberStack {
136 base: BasePtr(start_ptr),
137 len: range.len(),
138 storage: FiberStackStorage::Custom(custom),
139 })
140 }
141
top(&self) -> Option<*mut u8>142 pub fn top(&self) -> Option<*mut u8> {
143 Some(self.base.0.wrapping_byte_add(self.len))
144 }
145
range(&self) -> Option<Range<usize>>146 pub fn range(&self) -> Option<Range<usize>> {
147 let base = self.base.0 as usize;
148 Some(base..base + self.len)
149 }
150
guard_range(&self) -> Option<Range<*mut u8>>151 pub fn guard_range(&self) -> Option<Range<*mut u8>> {
152 match &self.storage {
153 FiberStackStorage::Unmanaged(guard_size) => unsafe {
154 let start = self.base.0.sub(*guard_size);
155 Some(start..self.base.0)
156 },
157 FiberStackStorage::Mmap(mmap) => Some(mmap.mapping_base..self.base.0),
158 FiberStackStorage::Custom(custom) => Some(custom.guard_range()),
159 }
160 }
161 }
162
163 struct MmapFiberStack {
164 mapping_base: *mut u8,
165 mapping_len: usize,
166 }
167
168 unsafe impl Send for MmapFiberStack {}
169 unsafe impl Sync for MmapFiberStack {}
170
171 impl MmapFiberStack {
new(size: usize) -> Result<Self>172 fn new(size: usize) -> Result<Self> {
173 // Round up our stack size request to the nearest multiple of the
174 // page size.
175 let page_size = host_page_size();
176 let size = if size == 0 {
177 page_size
178 } else {
179 let with_extra = size
180 .checked_add(page_size - 1)
181 .ok_or_else(|| OutOfMemory::new(usize::MAX))?;
182 with_extra & (!(page_size - 1))
183 };
184
185 unsafe {
186 // Add in one page for a guard page and then ask for some memory.
187 let mmap_len = size + page_size;
188 let mmap = rustix::mm::mmap_anonymous(
189 ptr::null_mut(),
190 mmap_len,
191 rustix::mm::ProtFlags::empty(),
192 rustix::mm::MapFlags::PRIVATE,
193 )?;
194
195 rustix::mm::mprotect(
196 mmap.byte_add(page_size),
197 size,
198 rustix::mm::MprotectFlags::READ | rustix::mm::MprotectFlags::WRITE,
199 )?;
200
201 Ok(MmapFiberStack {
202 mapping_base: mmap.cast(),
203 mapping_len: mmap_len,
204 })
205 }
206 }
207 }
208
209 impl Drop for MmapFiberStack {
drop(&mut self)210 fn drop(&mut self) {
211 unsafe {
212 let ret = rustix::mm::munmap(self.mapping_base.cast(), self.mapping_len);
213 debug_assert!(ret.is_ok());
214 }
215 }
216 }
217
218 pub struct Fiber;
219
220 pub struct Suspend {
221 top_of_stack: *mut u8,
222 previous: asan::PreviousStack,
223 }
224
fiber_start<F, A, B, C>(arg0: *mut u8, top_of_stack: *mut u8) -> *mut u8 where F: FnOnce(A, &mut super::Suspend<A, B, C>) -> C,225 extern "C" fn fiber_start<F, A, B, C>(arg0: *mut u8, top_of_stack: *mut u8) -> *mut u8
226 where
227 F: FnOnce(A, &mut super::Suspend<A, B, C>) -> C,
228 {
229 unsafe {
230 // Complete the `start_switch` AddressSanitizer handshake which would
231 // have been started in `Fiber::resume`.
232 let previous = asan::fiber_start_complete();
233
234 let inner = Suspend {
235 top_of_stack,
236 previous,
237 };
238 let initial = inner.take_resume::<A, B, C>();
239 let mut inner =
240 super::Suspend::<A, B, C>::execute(inner, initial, Box::from_raw(arg0.cast::<F>()));
241 asan::fiber_exit(&mut inner.previous);
242 inner.top_of_stack
243 }
244 }
245
246 impl Fiber {
new<F, A, B, C>(stack: &FiberStack, func: F) -> Result<Self> where F: FnOnce(A, &mut super::Suspend<A, B, C>) -> C,247 pub fn new<F, A, B, C>(stack: &FiberStack, func: F) -> Result<Self>
248 where
249 F: FnOnce(A, &mut super::Suspend<A, B, C>) -> C,
250 {
251 // On unsupported platforms `wasmtime_fiber_init` is a panicking shim so
252 // return an error saying the host architecture isn't supported instead.
253 if !SUPPORTED_ARCH {
254 bail!("fibers not supported on this host architecture");
255 }
256 unsafe {
257 let data = Box::into_raw(try_new::<Box<_>>(func)?).cast();
258 wasmtime_fiber_init(stack.top().unwrap(), fiber_start::<F, A, B, C>, data);
259 }
260
261 Ok(Self)
262 }
263
resume<A, B, C>(&self, stack: &FiberStack, result: &Cell<RunResult<A, B, C>>)264 pub(crate) fn resume<A, B, C>(&self, stack: &FiberStack, result: &Cell<RunResult<A, B, C>>) {
265 unsafe {
266 // Store where our result is going at the very tip-top of the
267 // stack, otherwise known as our reserved slot for this information.
268 //
269 // In the diagram above this is updating address 0xAff8
270 let addr = stack.top().unwrap().cast::<usize>().offset(-1);
271 addr.write(result as *const _ as usize);
272
273 asan::fiber_switch(stack.top().unwrap(), &mut asan::PreviousStack::new(stack));
274
275 // null this out to help catch use-after-free
276 addr.write(0);
277 }
278 }
279
drop<A, B, C>(&mut self)280 pub(crate) unsafe fn drop<A, B, C>(&mut self) {}
281 }
282
283 impl Suspend {
switch<A, B, C>(&mut self, result: RunResult<A, B, C>) -> A284 pub(crate) fn switch<A, B, C>(&mut self, result: RunResult<A, B, C>) -> A {
285 unsafe {
286 // Calculate 0xAff8 and then write to it
287 (*self.result_location::<A, B, C>()).set(result);
288
289 asan::fiber_switch(self.top_of_stack, &mut self.previous);
290
291 self.take_resume::<A, B, C>()
292 }
293 }
294
start_exit<A, B, C>(&mut self, result: RunResult<A, B, C>)295 pub(crate) fn start_exit<A, B, C>(&mut self, result: RunResult<A, B, C>) {
296 unsafe {
297 (*self.result_location::<A, B, C>()).set(result);
298 }
299 }
300
take_resume<A, B, C>(&self) -> A301 unsafe fn take_resume<A, B, C>(&self) -> A {
302 unsafe {
303 match (*self.result_location::<A, B, C>()).replace(RunResult::Executing) {
304 RunResult::Resuming(val) => val,
305 _ => panic!("not in resuming state"),
306 }
307 }
308 }
309
result_location<A, B, C>(&self) -> *const Cell<RunResult<A, B, C>>310 unsafe fn result_location<A, B, C>(&self) -> *const Cell<RunResult<A, B, C>> {
311 unsafe {
312 let ret = self.top_of_stack.cast::<*const u8>().offset(-1).read();
313 assert!(!ret.is_null());
314 ret.cast()
315 }
316 }
317 }
318
319 /// Support for AddressSanitizer to support stack manipulations we do in this
320 /// fiber implementation.
321 ///
322 /// This module uses, when fuzzing is enabled, special intrinsics provided by
323 /// the sanitizer runtime called `__sanitizer_{start,finish}_switch_fiber`.
324 /// These aren't really super heavily documented and the current implementation
325 /// is inspired by googling the functions and looking at Boost & Julia's usage
326 /// of them as well as the documentation for these functions in their own
327 /// header file in the LLVM source tree. The general idea is that they're
328 /// called around every stack switch with some other fiddly bits as well.
329 #[cfg(asan)]
330 mod asan {
331 use super::*;
332 use alloc::boxed::Box;
333 use alloc::vec::Vec;
334 use std::mem::ManuallyDrop;
335 use std::ops::Range;
336 use std::sync::Mutex;
337
338 /// State for the "previous stack" maintained by asan itself and fed in for
339 /// custom stacks.
340 pub struct PreviousStack {
341 bottom: *const u8,
342 size: usize,
343 }
344
345 impl PreviousStack {
new(stack: &FiberStack) -> PreviousStack346 pub fn new(stack: &FiberStack) -> PreviousStack {
347 let range = stack.range().unwrap();
348 PreviousStack {
349 bottom: range.start as *const u8,
350 // Discount the two pointers we store at the top of the stack,
351 // so subtract two pointers.
352 size: range.len() - 2 * std::mem::size_of::<*const u8>(),
353 }
354 }
355 }
356
357 impl Default for PreviousStack {
default() -> PreviousStack358 fn default() -> PreviousStack {
359 PreviousStack {
360 bottom: std::ptr::null(),
361 size: 0,
362 }
363 }
364 }
365
366 /// Switches the current stack to `top_of_stack`
367 ///
368 /// * `top_of_stack` - for going to fibers this is calculated and for
369 /// restoring back to the original stack this was saved during the initial
370 /// transition.
371 /// * `is_finishing` - whether or not we're switching off a fiber for the
372 /// final time; customizes how asan intrinsics are invoked.
373 /// * `prev` - the stack we're switching to initially and saves the
374 /// stack to return to upon resumption.
fiber_switch(top_of_stack: *mut u8, prev: &mut PreviousStack)375 pub unsafe fn fiber_switch(top_of_stack: *mut u8, prev: &mut PreviousStack) {
376 assert!(super::SUPPORTED_ARCH);
377 let mut private_asan_pointer = std::ptr::null_mut();
378
379 // NB: in fiddling with asan an optimizations and such it appears that
380 // these functions need to be "very close to each other". If other Rust
381 // functions are invoked or added as an abstraction here that appears to
382 // trigger false positives in ASAN. That leads to the design of this
383 // module as-is where this function exists to have these three
384 // functions very close to one another.
385 unsafe {
386 __sanitizer_start_switch_fiber(Some(&mut private_asan_pointer), prev.bottom, prev.size);
387 super::wasmtime_fiber_switch(top_of_stack);
388 __sanitizer_finish_switch_fiber(private_asan_pointer, &mut prev.bottom, &mut prev.size);
389 }
390 }
391
fiber_exit(prev: &mut PreviousStack)392 pub unsafe fn fiber_exit(prev: &mut PreviousStack) {
393 assert!(super::SUPPORTED_ARCH);
394 unsafe {
395 __sanitizer_start_switch_fiber(None, prev.bottom, prev.size);
396 }
397 }
398
399 /// Hook for when a fiber first starts, used to configure ASAN.
fiber_start_complete() -> PreviousStack400 pub unsafe fn fiber_start_complete() -> PreviousStack {
401 let mut ret = PreviousStack::default();
402 unsafe {
403 __sanitizer_finish_switch_fiber(std::ptr::null_mut(), &mut ret.bottom, &mut ret.size);
404 }
405 ret
406 }
407
408 // These intrinsics are provided by the address sanitizer runtime. Their C
409 // signatures were translated into Rust-isms here with `Option` and `&mut`.
410 unsafe extern "C" {
__sanitizer_start_switch_fiber( private_asan_pointer_save: Option<&mut *mut u8>, bottom: *const u8, size: usize, )411 fn __sanitizer_start_switch_fiber(
412 private_asan_pointer_save: Option<&mut *mut u8>,
413 bottom: *const u8,
414 size: usize,
415 );
__sanitizer_finish_switch_fiber( private_asan_pointer: *mut u8, bottom_old: &mut *const u8, size_old: &mut usize, )416 fn __sanitizer_finish_switch_fiber(
417 private_asan_pointer: *mut u8,
418 bottom_old: &mut *const u8,
419 size_old: &mut usize,
420 );
421 }
422
423 /// This static is a workaround for llvm/llvm-project#53891, notably this is
424 /// a global cache of all fiber stacks.
425 ///
426 /// The problem with ASAN is that if we allocate memory for a stack, use it
427 /// as a stack, deallocate the stack, and then when that memory is later
428 /// mapped as normal heap memory. This is possible due to `mmap` reusing
429 /// addresses and it ends up confusing ASAN. In this situation ASAN will
430 /// have false positives about stack overflows saying that writes to
431 /// freshly-allocated memory, which just happened to historically be a
432 /// stack, are a stack overflow.
433 ///
434 /// This static works around the issue by ensuring that, only when asan is
435 /// enabled, all stacks are cached globally. Stacks are never deallocated
436 /// and forever retained here. This only works if the number of stacks
437 /// retained here is relatively small to prevent OOM from continuously
438 /// running programs. That's hopefully the case as ASAN is mostly used in
439 /// OSS-Fuzz and our fuzzers only fuzz one thing at a time per thread
440 /// meaning that this should only ever be a relatively small set of stacks.
441 static FIBER_STACKS: Mutex<Vec<MmapFiberStack>> = Mutex::new(Vec::new());
442
new_fiber_stack(size: usize) -> Result<Box<dyn RuntimeFiberStack>>443 pub fn new_fiber_stack(size: usize) -> Result<Box<dyn RuntimeFiberStack>> {
444 let page_size = host_page_size();
445 let needed_size = size
446 .checked_add(page_size)
447 .ok_or_else(|| OutOfMemory::new(usize::MAX))?;
448 let mut stacks = FIBER_STACKS.lock().unwrap();
449
450 let stack = match stacks.iter().position(|i| needed_size <= i.mapping_len) {
451 // If an appropriately sized stack was already allocated, then use
452 // that one.
453 Some(i) => stacks.remove(i),
454 // ... otherwise allocate a brand new stack.
455 None => MmapFiberStack::new(size)?,
456 };
457 let stack = AsanFiberStack {
458 mmap: ManuallyDrop::new(stack),
459 };
460 Ok(Box::new(stack))
461 }
462
463 /// Custom structure used to prevent the interior mmap-allocated stack from
464 /// actually getting unmapped.
465 ///
466 /// On drop this stack will return the interior stack to the global
467 /// `FIBER_STACKS` list.
468 struct AsanFiberStack {
469 mmap: ManuallyDrop<MmapFiberStack>,
470 }
471
472 unsafe impl RuntimeFiberStack for AsanFiberStack {
top(&self) -> *mut u8473 fn top(&self) -> *mut u8 {
474 self.mmap
475 .mapping_base
476 .wrapping_byte_add(self.mmap.mapping_len)
477 }
478
range(&self) -> Range<usize>479 fn range(&self) -> Range<usize> {
480 let base = self.mmap.mapping_base as usize;
481 let end = base + self.mmap.mapping_len;
482 base + host_page_size()..end
483 }
484
guard_range(&self) -> Range<*mut u8>485 fn guard_range(&self) -> Range<*mut u8> {
486 self.mmap.mapping_base..self.mmap.mapping_base.wrapping_add(host_page_size())
487 }
488 }
489
490 impl Drop for AsanFiberStack {
drop(&mut self)491 fn drop(&mut self) {
492 let stack = unsafe { ManuallyDrop::take(&mut self.mmap) };
493 FIBER_STACKS.lock().unwrap().push(stack);
494 }
495 }
496 }
497
498 // Shim module that's the same as above but only has stubs.
499 #[cfg(not(asan))]
500 mod asan_disabled {
501 use super::*;
502 use std::boxed::Box;
503
504 #[derive(Default)]
505 pub struct PreviousStack;
506
507 impl PreviousStack {
508 #[inline]
new(_stack: &FiberStack) -> PreviousStack509 pub fn new(_stack: &FiberStack) -> PreviousStack {
510 PreviousStack
511 }
512 }
513
fiber_switch(top_of_stack: *mut u8, _prev: &mut PreviousStack)514 pub unsafe fn fiber_switch(top_of_stack: *mut u8, _prev: &mut PreviousStack) {
515 assert!(super::SUPPORTED_ARCH);
516 unsafe {
517 super::wasmtime_fiber_switch(top_of_stack);
518 }
519 }
520
fiber_exit(_prev: &mut PreviousStack)521 pub unsafe fn fiber_exit(_prev: &mut PreviousStack) {}
522
523 #[inline]
fiber_start_complete() -> PreviousStack524 pub unsafe fn fiber_start_complete() -> PreviousStack {
525 PreviousStack
526 }
527
new_fiber_stack(_size: usize) -> Result<Box<dyn RuntimeFiberStack>>528 pub fn new_fiber_stack(_size: usize) -> Result<Box<dyn RuntimeFiberStack>> {
529 unimplemented!()
530 }
531 }
532
533 #[cfg(not(asan))]
534 use asan_disabled as asan;
535