xref: /wasmtime-44.0.1/crates/fiber/src/unix.rs (revision 122ddc71)
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