[go: up one dir, main page]

stacker/
lib.rs

1//! A library to help grow the stack when it runs out of space.
2//!
3//! This is an implementation of manually instrumented segmented stacks where points in a program's
4//! control flow are annotated with "maybe grow the stack here". Each point of annotation indicates
5//! how far away from the end of the stack it's allowed to be, plus the amount of stack to allocate
6//! if it does reach the end.
7//!
8//! Once a program has reached the end of its stack, a temporary stack on the heap is allocated and
9//! is switched to for the duration of a closure.
10//!
11//! For a set of lower-level primitives, consider the `psm` crate.
12//!
13//! # Examples
14//!
15//! ```
16//! // Grow the stack if we are within the "red zone" of 32K, and if we allocate
17//! // a new stack allocate 1MB of stack space.
18//! //
19//! // If we're already in bounds, just run the provided closure on current stack.
20//! stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
21//!     // guaranteed to have at least 32K of stack
22//! });
23//! ```
24
25#![allow(improper_ctypes)]
26
27#[macro_use]
28extern crate cfg_if;
29extern crate libc;
30#[cfg(windows)]
31extern crate windows_sys;
32#[macro_use]
33extern crate psm;
34
35mod backends;
36
37use std::cell::Cell;
38
39/// Grows the call stack if necessary.
40///
41/// This function is intended to be called at manually instrumented points in a program where
42/// recursion is known to happen quite a bit. This function will check to see if we're within
43/// `red_zone` bytes of the end of the stack, and if so it will allocate a new stack of at least
44/// `stack_size` bytes.
45///
46/// The closure `f` is guaranteed to run on a stack with at least `red_zone` bytes, and it will be
47/// run on the current stack if there's space available.
48#[inline(always)]
49pub fn maybe_grow<R, F: FnOnce() -> R>(red_zone: usize, stack_size: usize, callback: F) -> R {
50    // if we can't guess the remaining stack (unsupported on some platforms) we immediately grow
51    // the stack and then cache the new stack size (which we do know now because we allocated it.
52    let enough_space = match remaining_stack() {
53        Some(remaining) => remaining >= red_zone,
54        None => false,
55    };
56    if enough_space {
57        callback()
58    } else {
59        grow(stack_size, callback)
60    }
61}
62
63/// Always creates a new stack for the passed closure to run on.
64/// The closure will still be on the same thread as the caller of `grow`.
65/// This will allocate a new stack with at least `stack_size` bytes.
66pub fn grow<R, F: FnOnce() -> R>(stack_size: usize, callback: F) -> R {
67    // To avoid monomorphizing `_grow()` and everything it calls,
68    // we convert the generic callback to a dynamic one.
69    let mut opt_callback = Some(callback);
70    let mut ret = None;
71    let ret_ref = &mut ret;
72
73    // This wrapper around `callback` achieves two things:
74    // * It converts the `impl FnOnce` to a `dyn FnMut`.
75    //   `dyn` because we want it to not be generic, and
76    //   `FnMut` because we can't pass a `dyn FnOnce` around without boxing it.
77    // * It eliminates the generic return value, by writing it to the stack of this function.
78    //   Otherwise the closure would have to return an unsized value, which isn't possible.
79    let dyn_callback: &mut dyn FnMut() = &mut || {
80        let taken_callback = opt_callback.take().unwrap();
81        *ret_ref = Some(taken_callback());
82    };
83
84    _grow(stack_size, dyn_callback);
85    ret.unwrap()
86}
87
88/// Queries the amount of remaining stack as interpreted by this library.
89///
90/// This function will return the amount of stack space left which will be used
91/// to determine whether a stack switch should be made or not.
92pub fn remaining_stack() -> Option<usize> {
93    let current_ptr = current_stack_ptr();
94    get_stack_limit().map(|limit| current_ptr - limit)
95}
96
97psm_stack_information!(
98    yes {
99        fn current_stack_ptr() -> usize {
100            psm::stack_pointer() as usize
101        }
102    }
103    no {
104        #[inline(always)]
105        fn current_stack_ptr() -> usize {
106            unsafe {
107                let mut x = std::mem::MaybeUninit::<u8>::uninit();
108                // Unlikely to be ever exercised. As a fallback we execute a volatile read to a
109                // local (to hopefully defeat the optimisations that would make this local a static
110                // global) and take its address. This way we get a very approximate address of the
111                // current frame.
112                x.as_mut_ptr().write_volatile(42);
113                x.as_ptr() as usize
114            }
115        }
116    }
117);
118
119thread_local! {
120    static STACK_LIMIT: Cell<Option<usize>> = Cell::new(unsafe {
121        backends::guess_os_stack_limit()
122    })
123}
124
125#[inline(always)]
126fn get_stack_limit() -> Option<usize> {
127    STACK_LIMIT.with(|s| s.get())
128}
129
130#[inline(always)]
131#[allow(unused)]
132fn set_stack_limit(l: Option<usize>) {
133    STACK_LIMIT.with(|s| s.set(l))
134}
135
136psm_stack_manipulation! {
137    yes {
138        struct StackRestoreGuard {
139            new_stack: *mut std::ffi::c_void,
140            stack_bytes: usize,
141            old_stack_limit: Option<usize>,
142        }
143
144        impl StackRestoreGuard {
145            #[cfg(target_arch = "wasm32")]
146            unsafe fn new(stack_bytes: usize, _page_size: usize) -> StackRestoreGuard {
147                let layout = std::alloc::Layout::from_size_align(stack_bytes, 16).unwrap();
148                let ptr = std::alloc::alloc(layout);
149                assert!(!ptr.is_null(), "unable to allocate stack");
150                StackRestoreGuard {
151                    new_stack: ptr as *mut _,
152                    stack_bytes,
153                    old_stack_limit: get_stack_limit(),
154                }
155            }
156
157            #[cfg(not(target_arch = "wasm32"))]
158            unsafe fn new(stack_bytes: usize, page_size: usize) -> StackRestoreGuard {
159                let new_stack = libc::mmap(
160                    std::ptr::null_mut(),
161                    stack_bytes,
162                    libc::PROT_NONE,
163                    libc::MAP_PRIVATE |
164                    libc::MAP_ANON,
165                    -1, // Some implementations assert fd = -1 if MAP_ANON is specified
166                    0
167                );
168                assert_ne!(
169                    new_stack,
170                    libc::MAP_FAILED,
171                    "mmap failed to allocate stack: {}",
172                    std::io::Error::last_os_error()
173                );
174                let guard = StackRestoreGuard {
175                    new_stack,
176                    stack_bytes,
177                    old_stack_limit: get_stack_limit(),
178                };
179                let above_guard_page = new_stack.add(page_size);
180                #[cfg(not(target_os = "openbsd"))]
181                let result = libc::mprotect(
182                    above_guard_page,
183                    stack_bytes - page_size,
184                    libc::PROT_READ | libc::PROT_WRITE
185                );
186                #[cfg(target_os = "openbsd")]
187                let result = if libc::mmap(
188                        above_guard_page,
189                        stack_bytes - page_size,
190                        libc::PROT_READ | libc::PROT_WRITE,
191                        libc::MAP_FIXED | libc::MAP_PRIVATE | libc::MAP_ANON | libc::MAP_STACK,
192                        -1,
193                        0) == above_guard_page {
194                    0
195                } else {
196                    -1
197                };
198                assert_ne!(
199                    result,
200                    -1,
201                    "mprotect/mmap failed: {}",
202                    std::io::Error::last_os_error()
203                );
204                guard
205            }
206        }
207
208        impl Drop for StackRestoreGuard {
209            fn drop(&mut self) {
210                #[cfg(target_arch = "wasm32")]
211                unsafe {
212                    std::alloc::dealloc(
213                        self.new_stack as *mut u8,
214                        std::alloc::Layout::from_size_align_unchecked(self.stack_bytes, 16),
215                    );
216                }
217                #[cfg(not(target_arch = "wasm32"))]
218                unsafe {
219                    // FIXME: check the error code and decide what to do with it.
220                    // Perhaps a debug_assertion?
221                    libc::munmap(self.new_stack, self.stack_bytes);
222                }
223                set_stack_limit(self.old_stack_limit);
224            }
225        }
226
227        fn _grow(stack_size: usize, callback: &mut dyn FnMut()) {
228            // Calculate a number of pages we want to allocate for the new stack.
229            // For maximum portability we want to produce a stack that is aligned to a page and has
230            // a size that’s a multiple of page size. Furthermore we want to allocate two extras pages
231            // for the stack guard. To achieve that we do our calculations in number of pages and
232            // convert to bytes last.
233            let page_size = page_size();
234            let requested_pages = stack_size
235                .checked_add(page_size - 1)
236                .expect("unreasonably large stack requested") / page_size;
237            let stack_pages = std::cmp::max(1, requested_pages) + 2;
238            let stack_bytes = stack_pages.checked_mul(page_size)
239                .expect("unreasonably large stack requested");
240
241            // Next, there are a couple of approaches to how we allocate the new stack. We take the
242            // most obvious path and use `mmap`. We also `mprotect` a guard page into our
243            // allocation.
244            //
245            // We use a guard pattern to ensure we deallocate the allocated stack when we leave
246            // this function and also try to uphold various safety invariants required by `psm`
247            // (such as not unwinding from the callback we pass to it).
248            //
249            // Other than that this code has no meaningful gotchas.
250            unsafe {
251                let guard = StackRestoreGuard::new(stack_bytes, page_size);
252                let above_guard_page = guard.new_stack.add(page_size);
253                set_stack_limit(Some(above_guard_page as usize));
254                let panic = psm::on_stack(above_guard_page as *mut _, stack_size, move || {
255                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback)).err()
256                });
257                drop(guard);
258                if let Some(p) = panic {
259                    std::panic::resume_unwind(p);
260                }
261            }
262        }
263
264        fn page_size() -> usize {
265            // FIXME: consider caching the page size.
266            #[cfg(not(target_arch = "wasm32"))]
267            unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) as usize }
268            #[cfg(target_arch = "wasm32")]
269            { 65536 }
270        }
271    }
272
273    no {
274        #[cfg(not(windows))]
275        fn _grow(stack_size: usize, callback: &mut dyn FnMut()) {
276            let _ = stack_size;
277            callback();
278        }
279        #[cfg(windows)]
280        use backends::windows::_grow;
281    }
282}