Skip to main content

fallow_extract/
stack.rs

1//! Stack sizing for the parse and the walks of one user source.
2//!
3//! The Oxc parser and the Fallow AST walks recurse once per nesting level,
4//! and a stack overflow aborts the process. Each nesting level uses at least
5//! one source byte, and the stack use per source byte is bounded (see
6//! `docs/reference/extract-internals.md`). So a stack sized from the source
7//! length cannot overflow. A parse runs on the current thread when that
8//! thread has enough stack left, and on a scoped thread of the needed size
9//! otherwise.
10
11use std::cell::Cell;
12
13/// Stack bytes reserved per source byte.
14///
15/// A shape without brackets uses at most about 570 bytes per source byte in
16/// a release build and about 1350 in a debug build. AddressSanitizer pads
17/// every frame, so the fuzz build reserves 8 times the debug factor.
18const STACK_PER_SOURCE_BYTE: usize = if cfg!(fuzzing) {
19    8 * 2048
20} else if cfg!(debug_assertions) {
21    2048
22} else {
23    1024
24};
25
26/// Stack bytes reserved per level of bracket nesting, on top of the length
27/// term. An unclosed `(` costs one source byte and about 1900 stack bytes per
28/// level in a release build. The nesting guard caps the depth, so this term
29/// adds at most a few MiB.
30const STACK_PER_BRACKET_LEVEL: usize = if cfg!(fuzzing) { 8 * 4096 } else { 4096 };
31
32/// Stack for the frames around the parse: the caller, the walks that do not
33/// recurse with the source, and the thread start.
34const BASE_STACK: usize = 256 * 1024;
35
36/// Largest stack a parse asks for. The stack is a reservation: a parse
37/// touches only the pages that its depth uses. A larger need is refused, so
38/// the file counts as too deep instead of parsing on too small a stack. In a
39/// 64-bit release build only a source over 64 MB reaches the cap, which needs
40/// `--max-file-size 0` or a raised limit. A thread with such a stack can fail
41/// to start under strict memory limits; the parse then runs on the current
42/// thread.
43const MAX_STACK: usize = if cfg!(target_pointer_width = "64") {
44    64 << 30
45} else {
46    1 << 30
47};
48
49thread_local! {
50    /// Address near the top of this thread's stack and the stack size, when
51    /// the thread was started with a known size.
52    static KNOWN_STACK: Cell<Option<(usize, usize)>> = const { Cell::new(None) };
53}
54
55/// Record that the current thread was started with `size` bytes of stack.
56///
57/// Call it first thing in the thread, for example from a Rayon
58/// `start_handler`. A thread that never calls it counts as having no stack
59/// to spare, so each bounded parse on it gets its own thread.
60pub fn register_thread_stack(size: usize) {
61    let top = stack_address();
62    KNOWN_STACK.with(|known| known.set(Some((top, size))));
63}
64
65/// Stack that a parse of `source_len` bytes with `bracket_depth` nested
66/// brackets can use, or `None` when that is more than the cap.
67pub fn stack_need(source_len: usize, bracket_depth: usize) -> Option<usize> {
68    let need = source_len
69        .saturating_mul(STACK_PER_SOURCE_BYTE)
70        .saturating_add(bracket_depth.saturating_mul(STACK_PER_BRACKET_LEVEL))
71        .saturating_add(BASE_STACK);
72    (need <= MAX_STACK).then_some(need)
73}
74
75/// Run `run` with at least `need` bytes of stack.
76///
77/// When the sized thread cannot start, `run` runs on the current thread, as
78/// it did before stack sizing existed.
79pub fn run_with_stack<R: Send>(need: usize, run: impl FnOnce() -> R + Send) -> R {
80    if remaining_stack() >= need {
81        return run();
82    }
83    // The closure must survive a failed spawn, so the thread takes it from
84    // a shared slot instead of by value.
85    let slot = std::sync::Mutex::new(Some(run));
86    let take = || {
87        slot.lock()
88            .unwrap_or_else(std::sync::PoisonError::into_inner)
89            .take()
90    };
91    let spawned = std::thread::scope(|scope| {
92        std::thread::Builder::new()
93            .stack_size(need)
94            .spawn_scoped(scope, || {
95                register_thread_stack(need);
96                take().map(|run| run())
97            })
98            .map(|handle| {
99                handle
100                    .join()
101                    .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
102            })
103    });
104    match spawned {
105        Ok(Some(result)) => result,
106        Ok(None) | Err(_) => match take() {
107            Some(run) => run(),
108            None => unreachable!("the spawned thread took the closure and returned its result"),
109        },
110    }
111}
112
113/// Stack left on the current thread, or 0 when its size is unknown.
114fn remaining_stack() -> usize {
115    KNOWN_STACK.with(Cell::get).map_or(0, |(top, size)| {
116        // Every supported target grows the stack downward.
117        let used = top.saturating_sub(stack_address());
118        size.saturating_sub(used)
119    })
120}
121
122#[inline(never)]
123fn stack_address() -> usize {
124    let marker = 0_u8;
125    std::ptr::from_ref(std::hint::black_box(&marker)) as usize
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    const PROBE_STACK: usize = 4 << 20;
133
134    fn on_registered_thread(probe: impl FnOnce() + Send + 'static) {
135        std::thread::Builder::new()
136            .stack_size(PROBE_STACK)
137            .spawn(move || {
138                register_thread_stack(PROBE_STACK);
139                probe();
140            })
141            .expect("probe thread should start")
142            .join()
143            .expect("probe thread should finish");
144    }
145
146    #[test]
147    fn need_grows_with_length_and_brackets() {
148        assert_eq!(stack_need(0, 0), Some(BASE_STACK));
149        assert!(stack_need(1000, 0) > stack_need(100, 0));
150        assert!(stack_need(1000, 500) > stack_need(1000, 0));
151        assert_eq!(stack_need(usize::MAX, usize::MAX), None);
152        assert_eq!(stack_need(MAX_STACK / STACK_PER_SOURCE_BYTE + 1, 0), None);
153    }
154
155    #[test]
156    fn unknown_thread_has_no_stack_to_spare() {
157        std::thread::spawn(|| assert_eq!(remaining_stack(), 0))
158            .join()
159            .expect("probe thread should finish");
160    }
161
162    #[test]
163    fn registered_thread_reports_its_stack() {
164        on_registered_thread(|| {
165            let remaining = remaining_stack();
166            assert!(remaining > PROBE_STACK - (1 << 20) && remaining <= PROBE_STACK);
167        });
168    }
169
170    #[test]
171    fn small_need_runs_on_the_current_thread() {
172        on_registered_thread(|| {
173            let caller = std::thread::current().id();
174            assert_eq!(
175                run_with_stack(1 << 20, || std::thread::current().id()),
176                caller
177            );
178        });
179    }
180
181    #[test]
182    fn large_need_runs_on_a_sized_thread() {
183        let caller = std::thread::current().id();
184        let (ran_on, remaining) = run_with_stack(64 << 20, || {
185            (std::thread::current().id(), remaining_stack())
186        });
187        assert_ne!(ran_on, caller);
188        assert!(remaining > 60 << 20);
189    }
190}