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}