Skip to main content

wasmer_vm/trap/
traphandlers.rs

1// This file contains code from external sources.
2// Attributions: https://github.com/wasmerio/wasmer/blob/main/docs/ATTRIBUTIONS.md
3
4#![allow(static_mut_refs)]
5
6//! WebAssembly trap handling, which is built on top of the lower-level
7//! signalhandling mechanisms.
8
9use super::trap::UnwindReason;
10use crate::Trap;
11#[cfg(all(unix, feature = "experimental-host-interrupt"))]
12use crate::interrupt_registry;
13use backtrace::Backtrace;
14use bytesize::ByteSize;
15use core::ptr::{read, read_unaligned};
16use corosensei::stack::{DefaultStack, Stack};
17use corosensei::trap::{CoroutineTrapHandler, TrapHandlerRegs};
18use corosensei::{CoroutineResult, ScopedCoroutine, Yielder};
19use scopeguard::defer;
20use std::any::Any;
21use std::cell::Cell;
22use std::error::Error;
23use std::io;
24use std::mem;
25#[cfg(unix)]
26use std::mem::MaybeUninit;
27use std::ptr::{self, NonNull};
28use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering, compiler_fence};
29use std::sync::{LazyLock, Once};
30use wasmer_types::TrapCode;
31
32/// Convenience extension for [`Stack`] that exposes the total mapped size.
33trait StackExt: Stack {
34    /// Returns the total size of the stack mapping (including guard page).
35    fn size(&self) -> usize {
36        self.base().get() - self.limit().get()
37    }
38}
39impl<T: Stack> StackExt for T {}
40
41/// Configuration for the runtime VM
42/// Currently only the stack size is configurable
43pub struct VMConfig {
44    /// Optional stack size (in byte) of the VM. Value lower than 8K will be rounded to 8K.
45    pub wasm_stack_size: Option<usize>,
46}
47
48// TrapInformation can be stored in the "Undefined Instruction" itself.
49// On x86_64, 0xC? select a "Register" for the Mod R/M part of "ud1" (so with no other bytes after)
50// On Arm64, the udf allows for a 16bits values, so we'll use the same 0xC? to store the trapinfo
51static MAGIC: u8 = 0xc0;
52
53static DEFAULT_STACK_SIZE: AtomicUsize = AtomicUsize::new(ByteSize::mib(1).as_u64() as usize);
54
55/// Maximum allowed default stack size (100 MiB) for the process-wide
56/// configuration set via `set_stack_size`.
57pub const MAX_STACK_SIZE: usize = ByteSize::mib(100).as_u64() as usize;
58
59// Current definition of `ucontext_t` in the `libc` crate is incorrect
60// on aarch64-apple-drawing so it's defined here with a more accurate definition.
61#[repr(C)]
62#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
63#[allow(non_camel_case_types)]
64struct ucontext_t {
65    uc_onstack: libc::c_int,
66    uc_sigmask: libc::sigset_t,
67    uc_stack: libc::stack_t,
68    uc_link: *mut libc::ucontext_t,
69    uc_mcsize: usize,
70    uc_mcontext: libc::mcontext_t,
71}
72
73#[cfg(all(unix, not(all(target_arch = "aarch64", target_os = "macos"))))]
74use libc::ucontext_t;
75
76/// Sets the process-wide default stack size for new Wasmer coroutines.
77/// The value is clamped to [8 KiB, MAX_STACK_SIZE].
78pub fn set_stack_size(size: usize) {
79    DEFAULT_STACK_SIZE.store(
80        size.clamp(ByteSize::kib(8).as_u64() as usize, MAX_STACK_SIZE),
81        Ordering::Relaxed,
82    );
83}
84
85/// Returns the process-wide default stack size in bytes.
86pub fn get_stack_size() -> usize {
87    DEFAULT_STACK_SIZE.load(Ordering::Relaxed)
88}
89
90/// Pool of pre-allocated coroutine stacks to avoid repeated mmap syscalls.
91/// Acts as the cross-thread overflow store; per-thread reuse is served by
92/// `TLS_STACK` to keep the hot path atomic-free.
93static STACK_POOL: LazyLock<crossbeam_queue::SegQueue<DefaultStack>> =
94    LazyLock::new(crossbeam_queue::SegQueue::new);
95
96/// Per-thread cache holding a single ready-to-use coroutine stack. The hot
97/// path of `on_wasm_stack` pops from here without touching the global
98/// `STACK_POOL`'s atomics; only the first call on a thread or re-entrant
99/// nested calls fall back to the pool.
100///
101/// On thread exit the held stack (if any) is pushed to `STACK_POOL` so memory
102/// cycles correctly across thread lifetimes (no mmap leaks).
103struct StackCache(Cell<Option<DefaultStack>>);
104
105impl Drop for StackCache {
106    fn drop(&mut self) {
107        if let Some(stack) = self.0.take() {
108            STACK_POOL.push(stack);
109        }
110    }
111}
112
113thread_local! {
114    static TLS_STACK: StackCache = const { StackCache(Cell::new(None)) };
115}
116
117/// Acquire a coroutine stack large enough for `min_size`. Prefers the
118/// thread-local cache (no atomics), falls back to the global `STACK_POOL`,
119/// then allocates a fresh stack.
120fn acquire_stack(min_size: usize) -> DefaultStack {
121    // Fast path: thread-local cache. Steady-state per-thread reuse never
122    // touches the SegQueue.
123    if let Some(stack) = TLS_STACK.with(|cache| cache.0.take()) {
124        if stack.size() >= min_size {
125            return stack;
126        }
127        // Undersized — discard (mirrors the existing `STACK_POOL.pop().filter(...)`
128        // behavior of not holding undersized stacks in rotation).
129        drop(stack);
130    }
131    // Cross-thread overflow pool. Single pop, single filter — same semantics
132    // as the pre-TLS implementation.
133    STACK_POOL
134        .pop()
135        .filter(|s| s.size() >= min_size)
136        .unwrap_or_else(|| DefaultStack::new(min_size).unwrap())
137}
138
139/// Release a coroutine stack. Prefers the thread-local slot if empty (no
140/// atomics); otherwise pushes to the global `STACK_POOL` so the stack is
141/// still reusable by other threads.
142fn release_stack(stack: DefaultStack) {
143    let displaced = TLS_STACK.with(|cache| cache.0.replace(Some(stack)));
144    if let Some(displaced) = displaced {
145        STACK_POOL.push(displaced);
146    }
147}
148
149/// Drains the coroutine stack pool at the moment it runs.
150///
151/// This is intended to be called before retrying with a larger stack size so
152/// that the pool does not keep serving cached undersized stacks.
153///
154/// Note that `STACK_POOL` is a global, concurrently used queue and that each
155/// thread also keeps a private cached stack in TLS. Other threads may push
156/// stacks back into the pool (for example, when their Wasm execution
157/// finishes) while or after this function is running, and TLS-cached stacks
158/// on other threads are not touched. As a result, this function provides
159/// only a best-effort drain: there is no guarantee that no undersized stacks
160/// exist immediately after it returns unless the caller ensures, via external
161/// synchronization, that no other Wasm executions can return stacks to the
162/// pool while this function runs. The current thread's TLS-cached stack is
163/// drained as part of this call.
164pub fn drain_stack_pool() {
165    // Drain the calling thread's TLS slot first (best-effort across threads
166    // still applies — other threads' caches aren't touched).
167    if let Some(stack) = TLS_STACK.with(|cache| cache.0.take()) {
168        drop(stack);
169    }
170    while STACK_POOL.pop().is_some() {}
171}
172
173cfg_select! {
174    unix => {
175        /// Function which may handle custom signals while processing traps.
176        pub type TrapHandlerFn<'a> = dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool + Send + Sync + 'a;
177    }
178    target_os = "windows" => {
179        /// Function which may handle custom signals while processing traps.
180        pub type TrapHandlerFn<'a> = dyn Fn(*mut windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS) -> bool + Send + Sync + 'a;
181    }
182}
183
184// Process an IllegalOpcode to see if it has a TrapCode payload
185unsafe fn process_illegal_op(addr: usize) -> Option<TrapCode> {
186    let mut val: Option<u8> = None;
187    unsafe {
188        if cfg!(target_arch = "x86_64") {
189            val = if read(addr as *mut u8) & 0xf0 == 0x40
190                && read((addr + 1) as *mut u8) == 0x0f
191                && read((addr + 2) as *mut u8) == 0xb9
192            {
193                Some(read((addr + 3) as *mut u8))
194            } else if read(addr as *mut u8) == 0x0f && read((addr + 1) as *mut u8) == 0xb9 {
195                Some(read((addr + 2) as *mut u8))
196            } else {
197                None
198            }
199        }
200        if cfg!(target_arch = "aarch64") {
201            val = if read_unaligned(addr as *mut u32) & 0xffff0000 == 0 {
202                Some(read(addr as *mut u8))
203            } else {
204                None
205            }
206        }
207        if cfg!(target_arch = "riscv64") {
208            let addr = addr as *mut u32;
209            // Check if 'unimp' instruction
210            val = if read(addr) == 0xc0001073 {
211                // Read from the instruction we emitted: 'addi a0, xzero, $payload'
212                // and take the encoded immediate value (upper 12-bits).
213                let prev_insn = read(addr.sub(1));
214                if (prev_insn & 0xffff) == 0x0513 {
215                    Some((prev_insn >> 20) as u8)
216                } else {
217                    None
218                }
219            } else {
220                None
221            };
222        }
223    }
224
225    // The direct encoding of a trap into the instruction is unused on RISC-V:
226    if cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64") {
227        val = val.and_then(|val| {
228            if val & MAGIC == MAGIC {
229                Some(val & 0xf)
230            } else {
231                None
232            }
233        });
234    }
235
236    match val {
237        None => None,
238        Some(val) => match val {
239            0 => Some(TrapCode::StackOverflow),
240            1 => Some(TrapCode::HeapAccessOutOfBounds),
241            2 => Some(TrapCode::HeapMisaligned),
242            3 => Some(TrapCode::TableAccessOutOfBounds),
243            4 => Some(TrapCode::IndirectCallToNull),
244            5 => Some(TrapCode::BadSignature),
245            6 => Some(TrapCode::IntegerOverflow),
246            7 => Some(TrapCode::IntegerDivisionByZero),
247            8 => Some(TrapCode::BadConversionToInteger),
248            9 => Some(TrapCode::UnreachableCodeReached),
249            10 => Some(TrapCode::UnalignedAtomic),
250            _ => None,
251        },
252    }
253}
254
255cfg_select! {
256    unix => {
257        static mut PREV_SIGSEGV: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
258        static mut PREV_SIGBUS: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
259        static mut PREV_SIGILL: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
260        static mut PREV_SIGFPE: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
261
262        #[cfg(feature = "experimental-host-interrupt")]
263        static mut PREV_SIGUSR1: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
264
265        unsafe fn platform_init() { unsafe {
266            let register = |slot: &mut MaybeUninit<libc::sigaction>, signal: i32, nodefer: bool| {
267                let mut handler: libc::sigaction = mem::zeroed();
268                // The flags here are relatively careful, and they are...
269                //
270                // SA_SIGINFO gives us access to information like the program
271                // counter from where the fault happened.
272                //
273                // SA_ONSTACK allows us to handle signals on an alternate stack,
274                // so that the handler can run in response to running out of
275                // stack space on the main stack. Rust installs an alternate
276                // stack with sigaltstack, so we rely on that.
277                //
278                // SA_NODEFER allows us to reenter the signal handler if we
279                // crash while handling the signal, and fall through to the
280                // Breakpad handler by testing handlingSegFault.
281                handler.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
282                if nodefer {
283                    handler.sa_flags |= libc::SA_NODEFER;
284                }
285                handler.sa_sigaction = trap_handler as *const () as usize;
286                libc::sigemptyset(&mut handler.sa_mask);
287                if libc::sigaction(signal, &handler, slot.as_mut_ptr()) != 0 {
288                    panic!(
289                        "unable to install signal handler: {}",
290                        io::Error::last_os_error(),
291                    );
292                }
293            };
294
295            // Allow handling OOB with signals on all architectures
296            register(&mut PREV_SIGSEGV, libc::SIGSEGV, true);
297
298            // Handle `unreachable` instructions which execute `ud2` right now
299            register(&mut PREV_SIGILL, libc::SIGILL, true);
300
301            // SIGUSR1 is used to interrupt long-running WASM code.
302            // It doesn't use NODEFER since, if a second interruption
303            // request comes in while one is already being processed,
304            // there's nothing meaningful we can do.
305            #[cfg(feature = "experimental-host-interrupt")]
306            register(&mut PREV_SIGUSR1, libc::SIGUSR1, false);
307
308            // x86 uses SIGFPE to report division by zero
309            if cfg!(target_arch = "x86") || cfg!(target_arch = "x86_64") {
310                register(&mut PREV_SIGFPE, libc::SIGFPE, true);
311            }
312
313            // On ARM, handle Unaligned Accesses.
314            // On Darwin, guard page accesses are raised as SIGBUS.
315            if cfg!(target_arch = "arm") || cfg!(target_vendor = "apple") {
316                register(&mut PREV_SIGBUS, libc::SIGBUS, true);
317            }
318
319            // This is necessary to support debugging under LLDB on Darwin.
320            // For more details see https://github.com/mono/mono/commit/8e75f5a28e6537e56ad70bf870b86e22539c2fb7
321            #[cfg(target_vendor = "apple")]
322            {
323                use mach2::exception_types::*;
324                use mach2::kern_return::*;
325                use mach2::port::*;
326                use mach2::thread_status::*;
327                use mach2::traps::*;
328                use mach2::mach_types::*;
329
330                unsafe extern "C" {
331                    fn task_set_exception_ports(
332                        task: task_t,
333                        exception_mask: exception_mask_t,
334                        new_port: mach_port_t,
335                        behavior: exception_behavior_t,
336                        new_flavor: thread_state_flavor_t,
337                    ) -> kern_return_t;
338                }
339
340                #[allow(non_snake_case)]
341                #[cfg(target_arch = "x86_64")]
342                let MACHINE_THREAD_STATE = x86_THREAD_STATE64;
343                #[allow(non_snake_case)]
344                #[cfg(target_arch = "aarch64")]
345                let MACHINE_THREAD_STATE = 6;
346
347                task_set_exception_ports(
348                    mach_task_self(),
349                    EXC_MASK_BAD_ACCESS | EXC_MASK_ARITHMETIC | EXC_MASK_BAD_INSTRUCTION,
350                    MACH_PORT_NULL,
351                    EXCEPTION_STATE_IDENTITY as exception_behavior_t,
352                    MACHINE_THREAD_STATE,
353                );
354            }
355        }}
356
357        unsafe extern "C" fn trap_handler(
358            signum: libc::c_int,
359            siginfo: *mut libc::siginfo_t,
360            context: *mut libc::c_void,
361        ) { unsafe {
362            let previous = match signum {
363                libc::SIGSEGV => &PREV_SIGSEGV,
364                libc::SIGBUS => &PREV_SIGBUS,
365                libc::SIGFPE => &PREV_SIGFPE,
366                libc::SIGILL => &PREV_SIGILL,
367                #[cfg(feature = "experimental-host-interrupt")]
368                libc::SIGUSR1 => &PREV_SIGUSR1,
369                _ => panic!("unknown signal: {signum}"),
370            };
371            // We try to get the fault address associated to this signal
372            let maybe_fault_address = match signum {
373                libc::SIGSEGV | libc::SIGBUS => {
374                    Some((*siginfo).si_addr() as usize)
375                }
376                _ => None,
377            };
378            let trap_code = match signum {
379                // check if it was cased by a UD and if the Trap info is a payload to it
380                libc::SIGILL => {
381                    let addr = (*siginfo).si_addr() as usize;
382                    process_illegal_op(addr)
383                }
384                #[cfg(feature = "experimental-host-interrupt")]
385                libc::SIGUSR1 => {
386                    // If we're not running WASM code from the specific store for which
387                    // an interrupt was requested, there's nothing to do.
388                    if !interrupt_registry::on_interrupted() {
389                        return;
390                    }
391                    Some(TrapCode::HostInterrupt)
392                }
393                _ => None,
394            };
395            let ucontext = &mut *(context as *mut ucontext_t);
396            let (pc, sp) = get_pc_sp(ucontext);
397            let handled = TrapHandlerContext::handle_trap(
398                pc,
399                sp,
400                maybe_fault_address,
401                trap_code,
402                |regs| update_context(ucontext, regs),
403                |handler| handler(signum, siginfo, context),
404            );
405
406            if handled {
407                return;
408            }
409
410            // If we're not running WASM code at all, there's nothing to
411            // do for an interrupt.
412            #[cfg(feature = "experimental-host-interrupt")]
413            if signum == libc::SIGUSR1 {
414                return;
415            }
416
417            // This signal is not for any compiled wasm code we expect, so we
418            // need to forward the signal to the next handler. If there is no
419            // next handler (SIG_IGN or SIG_DFL), then it's time to crash. To do
420            // this, we set the signal back to its original disposition and
421            // return. This will cause the faulting op to be re-executed which
422            // will crash in the normal way. If there is a next handler, call
423            // it. It will either crash synchronously, fix up the instruction
424            // so that execution can continue and return, or trigger a crash by
425            // returning the signal to it's original disposition and returning.
426            let previous = &*previous.as_ptr();
427            if previous.sa_flags & libc::SA_SIGINFO != 0 {
428                mem::transmute::<
429                    usize,
430                    extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void),
431                >(previous.sa_sigaction)(signum, siginfo, context)
432            } else if previous.sa_sigaction == libc::SIG_DFL
433            {
434                libc::sigaction(signum, previous, ptr::null_mut());
435            } else if previous.sa_sigaction != libc::SIG_IGN {
436                mem::transmute::<usize, extern "C" fn(libc::c_int)>(
437                    previous.sa_sigaction
438                )(signum)
439            }
440        }}
441
442        unsafe fn get_pc_sp(context: &ucontext_t) -> (usize, usize) {
443            let (pc, sp);
444            cfg_select! {
445                all(
446                    any(target_os = "linux", target_os = "android"),
447                    target_arch = "x86_64",
448                ) => {
449                    pc = context.uc_mcontext.gregs[libc::REG_RIP as usize] as usize;
450                    sp = context.uc_mcontext.gregs[libc::REG_RSP as usize] as usize;
451                }
452                all(
453                    any(target_os = "linux", target_os = "android"),
454                    target_arch = "x86",
455                ) => {
456                    pc = context.uc_mcontext.gregs[libc::REG_EIP as usize] as usize;
457                    sp = context.uc_mcontext.gregs[libc::REG_ESP as usize] as usize;
458                }
459                all(target_os = "freebsd", target_arch = "x86") => {
460                    pc = context.uc_mcontext.mc_eip as usize;
461                    sp = context.uc_mcontext.mc_esp as usize;
462                }
463                all(target_os = "freebsd", target_arch = "x86_64") => {
464                    pc = context.uc_mcontext.mc_rip as usize;
465                    sp = context.uc_mcontext.mc_rsp as usize;
466                }
467                all(target_vendor = "apple", target_arch = "x86_64") => {
468                    let mcontext = unsafe { &*context.uc_mcontext };
469                    pc = mcontext.__ss.__rip as usize;
470                    sp = mcontext.__ss.__rsp as usize;
471                }
472                all(
473                        any(target_os = "linux", target_os = "android"),
474                        target_arch = "aarch64",
475                    ) => {
476                    pc = context.uc_mcontext.pc as usize;
477                    sp = context.uc_mcontext.sp as usize;
478                }
479                all(
480                    any(target_os = "linux", target_os = "android"),
481                    target_arch = "arm",
482                ) => {
483                    pc = context.uc_mcontext.arm_pc as usize;
484                    sp = context.uc_mcontext.arm_sp as usize;
485                }
486                all(
487                    any(target_os = "linux", target_os = "android"),
488                    any(target_arch = "riscv64", target_arch = "riscv32"),
489                ) => {
490                    pc = context.uc_mcontext.__gregs[libc::REG_PC] as usize;
491                    sp = context.uc_mcontext.__gregs[libc::REG_SP] as usize;
492                }
493                all(target_vendor = "apple", target_arch = "aarch64") => {
494                    let mcontext = unsafe { &*context.uc_mcontext };
495                    pc = mcontext.__ss.__pc as usize;
496                    sp = mcontext.__ss.__sp as usize;
497                }
498                all(target_os = "freebsd", target_arch = "aarch64") => {
499                    pc = context.uc_mcontext.mc_gpregs.gp_elr as usize;
500                    sp = context.uc_mcontext.mc_gpregs.gp_sp as usize;
501                }
502                all(target_os = "linux", target_arch = "loongarch64") => {
503                    pc = context.uc_mcontext.__gregs[1] as usize;
504                    sp = context.uc_mcontext.__gregs[3] as usize;
505                }
506                all(target_os = "linux", target_arch = "powerpc64") => {
507                    pc = (*context.uc_mcontext.regs).nip as usize;
508                    sp = (*context.uc_mcontext.regs).gpr[1] as usize;
509                }
510                _ => {
511                    compile_error!("Unsupported platform");
512                }
513            };
514            (pc, sp)
515        }
516
517        unsafe fn update_context(context: &mut ucontext_t, regs: TrapHandlerRegs) {
518            cfg_select! {
519                all(
520                        any(target_os = "linux", target_os = "android"),
521                        target_arch = "x86_64",
522                    ) => {
523                    let TrapHandlerRegs { rip, rsp, rbp, rdi, rsi } = regs;
524                    context.uc_mcontext.gregs[libc::REG_RIP as usize] = rip as i64;
525                    context.uc_mcontext.gregs[libc::REG_RSP as usize] = rsp as i64;
526                    context.uc_mcontext.gregs[libc::REG_RBP as usize] = rbp as i64;
527                    context.uc_mcontext.gregs[libc::REG_RDI as usize] = rdi as i64;
528                    context.uc_mcontext.gregs[libc::REG_RSI as usize] = rsi as i64;
529                }
530                all(
531                    any(target_os = "linux", target_os = "android"),
532                    target_arch = "x86",
533                ) => {
534                    let TrapHandlerRegs { eip, esp, ebp, ecx, edx } = regs;
535                    context.uc_mcontext.gregs[libc::REG_EIP as usize] = eip as i32;
536                    context.uc_mcontext.gregs[libc::REG_ESP as usize] = esp as i32;
537                    context.uc_mcontext.gregs[libc::REG_EBP as usize] = ebp as i32;
538                    context.uc_mcontext.gregs[libc::REG_ECX as usize] = ecx as i32;
539                    context.uc_mcontext.gregs[libc::REG_EDX as usize] = edx as i32;
540                }
541                all(target_vendor = "apple", target_arch = "x86_64") => {
542                    let TrapHandlerRegs { rip, rsp, rbp, rdi, rsi } = regs;
543                    let mcontext = unsafe { &mut *context.uc_mcontext };
544                    mcontext.__ss.__rip = rip;
545                    mcontext.__ss.__rsp = rsp;
546                    mcontext.__ss.__rbp = rbp;
547                    mcontext.__ss.__rdi = rdi;
548                    mcontext.__ss.__rsi = rsi;
549                }
550                all(target_os = "freebsd", target_arch = "x86") => {
551                    let TrapHandlerRegs { eip, esp, ebp, ecx, edx } = regs;
552                    context.uc_mcontext.mc_eip = eip as libc::register_t;
553                    context.uc_mcontext.mc_esp = esp as libc::register_t;
554                    context.uc_mcontext.mc_ebp = ebp as libc::register_t;
555                    context.uc_mcontext.mc_ecx = ecx as libc::register_t;
556                    context.uc_mcontext.mc_edx = edx as libc::register_t;
557                }
558                all(target_os = "freebsd", target_arch = "x86_64") => {
559                    let TrapHandlerRegs { rip, rsp, rbp, rdi, rsi } = regs;
560                    context.uc_mcontext.mc_rip = rip as libc::register_t;
561                    context.uc_mcontext.mc_rsp = rsp as libc::register_t;
562                    context.uc_mcontext.mc_rbp = rbp as libc::register_t;
563                    context.uc_mcontext.mc_rdi = rdi as libc::register_t;
564                    context.uc_mcontext.mc_rsi = rsi as libc::register_t;
565                }
566                all(
567                        any(target_os = "linux", target_os = "android"),
568                        target_arch = "aarch64",
569                    ) => {
570                    let TrapHandlerRegs { pc, sp, x0, x1, x29, lr } = regs;
571                    context.uc_mcontext.pc = pc;
572                    context.uc_mcontext.sp = sp;
573                    context.uc_mcontext.regs[0] = x0;
574                    context.uc_mcontext.regs[1] = x1;
575                    context.uc_mcontext.regs[29] = x29;
576                    context.uc_mcontext.regs[30] = lr;
577                }
578                all(
579                        any(target_os = "linux", target_os = "android"),
580                        target_arch = "arm",
581                    ) => {
582                    let TrapHandlerRegs {
583                        pc,
584                        r0,
585                        r1,
586                        r7,
587                        r11,
588                        r13,
589                        r14,
590                        cpsr_thumb,
591                        cpsr_endian,
592                    } = regs;
593                    context.uc_mcontext.arm_pc = pc;
594                    context.uc_mcontext.arm_r0 = r0;
595                    context.uc_mcontext.arm_r1 = r1;
596                    context.uc_mcontext.arm_r7 = r7;
597                    context.uc_mcontext.arm_fp = r11;
598                    context.uc_mcontext.arm_sp = r13;
599                    context.uc_mcontext.arm_lr = r14;
600                    if cpsr_thumb {
601                        context.uc_mcontext.arm_cpsr |= 0x20;
602                    } else {
603                        context.uc_mcontext.arm_cpsr &= !0x20;
604                    }
605                    if cpsr_endian {
606                        context.uc_mcontext.arm_cpsr |= 0x200;
607                    } else {
608                        context.uc_mcontext.arm_cpsr &= !0x200;
609                    }
610                }
611                all(
612                    any(target_os = "linux", target_os = "android"),
613                    any(target_arch = "riscv64", target_arch = "riscv32"),
614                ) => {
615                    let TrapHandlerRegs { pc, ra, sp, a0, a1, s0 } = regs;
616                    context.uc_mcontext.__gregs[libc::REG_PC] = pc as libc::c_ulong;
617                    context.uc_mcontext.__gregs[libc::REG_RA] = ra as libc::c_ulong;
618                    context.uc_mcontext.__gregs[libc::REG_SP] = sp as libc::c_ulong;
619                    context.uc_mcontext.__gregs[libc::REG_A0] = a0 as libc::c_ulong;
620                    context.uc_mcontext.__gregs[libc::REG_A0 + 1] = a1 as libc::c_ulong;
621                    context.uc_mcontext.__gregs[libc::REG_S0] = s0 as libc::c_ulong;
622                }
623                all(target_vendor = "apple", target_arch = "aarch64") => {
624                    let TrapHandlerRegs { pc, sp, x0, x1, x29, lr } = regs;
625                    let mcontext = unsafe { &mut *context.uc_mcontext };
626                    mcontext.__ss.__pc = pc;
627                    mcontext.__ss.__sp = sp;
628                    mcontext.__ss.__x[0] = x0;
629                    mcontext.__ss.__x[1] = x1;
630                    mcontext.__ss.__fp = x29;
631                    mcontext.__ss.__lr = lr;
632                }
633                all(target_os = "freebsd", target_arch = "aarch64") => {
634                    let TrapHandlerRegs { pc, sp, x0, x1, x29, lr } = regs;
635                    context.uc_mcontext.mc_gpregs.gp_elr = pc as libc::register_t;
636                    context.uc_mcontext.mc_gpregs.gp_sp = sp as libc::register_t;
637                    context.uc_mcontext.mc_gpregs.gp_x[0] = x0 as libc::register_t;
638                    context.uc_mcontext.mc_gpregs.gp_x[1] = x1 as libc::register_t;
639                    context.uc_mcontext.mc_gpregs.gp_x[29] = x29 as libc::register_t;
640                    context.uc_mcontext.mc_gpregs.gp_lr = lr as libc::register_t;
641                }
642                all(target_os = "linux", target_arch = "loongarch64") => {
643                    let TrapHandlerRegs { pc, sp, a0, a1, fp, ra } = regs;
644                    context.uc_mcontext.__pc = pc;
645                    context.uc_mcontext.__gregs[1] = ra;
646                    context.uc_mcontext.__gregs[3] = sp;
647                    context.uc_mcontext.__gregs[4] = a0;
648                    context.uc_mcontext.__gregs[5] = a1;
649                    context.uc_mcontext.__gregs[22] = fp;
650                }
651                all(target_os = "linux", target_arch = "powerpc64") => {
652                    let TrapHandlerRegs { pc, sp, r3, r4, r31, lr } = regs;
653                    (*context.uc_mcontext.regs).nip = pc;
654                    (*context.uc_mcontext.regs).gpr[1] = sp;
655                    (*context.uc_mcontext.regs).gpr[3] = r3;
656                    (*context.uc_mcontext.regs).gpr[4] = r4;
657                    (*context.uc_mcontext.regs).gpr[31] = r31;
658                    (*context.uc_mcontext.regs).link = lr;
659                }
660                _ => {
661                    compile_error!("Unsupported platform");
662                }
663            };
664        }
665    }
666    target_os = "windows" => {
667        use windows_sys::Win32::System::Diagnostics::Debug::{
668            AddVectoredExceptionHandler,
669            CONTEXT,
670            EXCEPTION_CONTINUE_EXECUTION,
671            EXCEPTION_CONTINUE_SEARCH,
672            EXCEPTION_POINTERS,
673        };
674        use windows_sys::Win32::Foundation::{
675            EXCEPTION_ACCESS_VIOLATION,
676            EXCEPTION_ILLEGAL_INSTRUCTION,
677            EXCEPTION_INT_DIVIDE_BY_ZERO,
678            EXCEPTION_INT_OVERFLOW,
679            EXCEPTION_STACK_OVERFLOW,
680        };
681
682        unsafe fn platform_init() {
683            unsafe {
684                // our trap handler needs to go first, so that we can recover from
685                // wasm faults and continue execution, so pass `1` as a true value
686                // here.
687                let handler = AddVectoredExceptionHandler(1, Some(exception_handler));
688                if handler.is_null() {
689                    panic!("failed to add exception handler: {}", io::Error::last_os_error());
690                }
691            }
692        }
693
694        unsafe extern "system" fn exception_handler(
695            exception_info: *mut EXCEPTION_POINTERS
696        ) -> i32 {
697            unsafe {
698                // Check the kind of exception, since we only handle a subset within
699                // wasm code. If anything else happens we want to defer to whatever
700                // the rest of the system wants to do for this exception.
701                let record = &*(*exception_info).ExceptionRecord;
702                if record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION &&
703                    record.ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION &&
704                    record.ExceptionCode != EXCEPTION_STACK_OVERFLOW &&
705                    record.ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO &&
706                    record.ExceptionCode != EXCEPTION_INT_OVERFLOW
707                {
708                    return EXCEPTION_CONTINUE_SEARCH;
709                }
710
711                // FIXME: this is what the previous C++ did to make sure that TLS
712                // works by the time we execute this trap handling code. This isn't
713                // exactly super easy to call from Rust though and it's not clear we
714                // necessarily need to do so. Leaving this here in case we need this
715                // in the future, but for now we can probably wait until we see a
716                // strange fault before figuring out how to reimplement this in
717                // Rust.
718                //
719                // if (!NtCurrentTeb()->Reserved1[sThreadLocalArrayPointerIndex]) {
720                //     return EXCEPTION_CONTINUE_SEARCH;
721                // }
722
723                let context = &mut *(*exception_info).ContextRecord;
724                let (pc, sp) = get_pc_sp(context);
725
726                // We try to get the fault address associated to this exception.
727                let maybe_fault_address = match record.ExceptionCode {
728                    EXCEPTION_ACCESS_VIOLATION => Some(record.ExceptionInformation[1]),
729                    EXCEPTION_STACK_OVERFLOW => Some(sp),
730                    _ => None,
731                };
732                let trap_code = match record.ExceptionCode {
733                    // check if it was cased by a UD and if the Trap info is a payload to it
734                    EXCEPTION_ILLEGAL_INSTRUCTION => {
735                        process_illegal_op(pc)
736                    }
737                    _ => None,
738                };
739                // This is basically the same as the unix version above, only with a
740                // few parameters tweaked here and there.
741                let handled = TrapHandlerContext::handle_trap(
742                    pc,
743                    sp,
744                    maybe_fault_address,
745                    trap_code,
746                    |regs| update_context(context, regs),
747                    |handler| handler(exception_info),
748                );
749
750                if handled {
751                    EXCEPTION_CONTINUE_EXECUTION
752                } else {
753                    EXCEPTION_CONTINUE_SEARCH
754                }
755            }
756        }
757
758        unsafe fn get_pc_sp(context: &CONTEXT) -> (usize, usize) {
759            let (pc, sp);
760            cfg_select! {
761                target_arch = "x86_64" => {
762                    pc = context.Rip as usize;
763                    sp = context.Rsp as usize;
764                }
765                target_arch = "x86" => {
766                    pc = context.Eip as usize;
767                    sp = context.Esp as usize;
768                }
769                _ => {
770                    compile_error!("Unsupported platform");
771                }
772            };
773            (pc, sp)
774        }
775
776        unsafe fn update_context(context: &mut CONTEXT, regs: TrapHandlerRegs) {
777            cfg_select! {
778                target_arch = "x86_64" => {
779                    let TrapHandlerRegs { rip, rsp, rbp, rdi, rsi } = regs;
780                    context.Rip = rip;
781                    context.Rsp = rsp;
782                    context.Rbp = rbp;
783                    context.Rdi = rdi;
784                    context.Rsi = rsi;
785                }
786                target_arch = "x86" => {
787                    let TrapHandlerRegs { eip, esp, ebp, ecx, edx } = regs;
788                    context.Eip = eip;
789                    context.Esp = esp;
790                    context.Ebp = ebp;
791                    context.Ecx = ecx;
792                    context.Edx = edx;
793                }
794                _ => {
795                    compile_error!("Unsupported platform");
796                }
797            };
798        }
799    }
800}
801
802/// This function is required to be called before any WebAssembly is entered.
803/// This will configure global state such as signal handlers to prepare the
804/// process to receive wasm traps.
805///
806/// This function must not only be called globally once before entering
807/// WebAssembly but it must also be called once-per-thread that enters
808/// WebAssembly. Currently in wasmer's integration this function is called on
809/// creation of a `Store`.
810pub fn init_traps() {
811    // Miri cannot call `sigaction`/`sigemptyset`, and nothing under Miri
812    // executes compiled Wasm, so there is no trap for a handler to catch.
813    // Without this, building a `Store` at all is out of reach there, and the
814    // store-context tests that need one cannot run.
815    if cfg!(miri) {
816        return;
817    }
818    static INIT: Once = Once::new();
819    INIT.call_once(|| unsafe {
820        platform_init();
821    });
822}
823
824/// Raises a user-defined trap immediately.
825///
826/// This function performs as-if a wasm trap was just executed, only the trap
827/// has a dynamic payload associated with it which is user-provided. This trap
828/// payload is then returned from `catch_traps` below.
829///
830/// # Safety
831///
832/// Only safe to call when wasm code is on the stack, aka `catch_traps` must
833/// have been previous called and not yet returned.
834/// Additionally no Rust destructors may be on the stack.
835/// They will be skipped and not executed.
836pub unsafe fn raise_user_trap(data: Box<dyn Error + Send + Sync>) -> ! {
837    unsafe { unwind_with(UnwindReason::UserTrap(data)) }
838}
839
840/// Raises a trap from inside library code immediately.
841///
842/// This function performs as-if a wasm trap was just executed. This trap
843/// payload is then returned from `catch_traps` below.
844///
845/// # Safety
846///
847/// Only safe to call when wasm code is on the stack, aka `catch_traps` must
848/// have been previous called and not yet returned.
849/// Additionally no Rust destructors may be on the stack.
850/// They will be skipped and not executed.
851pub unsafe fn raise_lib_trap(trap: Trap) -> ! {
852    unsafe { unwind_with(UnwindReason::LibTrap(trap)) }
853}
854
855/// Carries a Rust panic across wasm code and resumes the panic on the other
856/// side.
857///
858/// # Safety
859///
860/// Only safe to call when wasm code is on the stack, aka `catch_traps` must
861/// have been previously called and not returned. Additionally no Rust destructors may be on the
862/// stack. They will be skipped and not executed.
863pub unsafe fn resume_panic(payload: Box<dyn Any + Send>) -> ! {
864    unsafe { unwind_with(UnwindReason::Panic(payload)) }
865}
866
867/// Catches any wasm traps that happen within the execution of `closure`,
868/// returning them as a `Result`.
869///
870/// # Safety
871///
872/// Highly unsafe since `closure` won't have any dtors run.
873pub unsafe fn catch_traps<F, R: 'static>(
874    trap_handler: Option<*const TrapHandlerFn<'static>>,
875    config: &VMConfig,
876    closure: F,
877) -> Result<R, Trap>
878where
879    F: FnOnce() -> R + 'static,
880{
881    // Ensure that per-thread initialization is done.
882    lazy_per_thread_init()?;
883    let stack_size = config
884        .wasm_stack_size
885        .unwrap_or_else(|| DEFAULT_STACK_SIZE.load(Ordering::Relaxed));
886    on_wasm_stack(stack_size, trap_handler, closure).map_err(UnwindReason::into_trap)
887}
888
889// We need two separate thread-local variables here:
890// - YIELDER is set within the new stack and is used to unwind back to the root
891//   of the stack from inside it.
892// - TRAP_HANDLER is set from outside the new stack and is solely used from
893//   signal handlers. It must be atomic since it is used by signal handlers.
894//
895// We also do per-thread signal stack initialization on the first time
896// TRAP_HANDLER is accessed.
897thread_local! {
898    static YIELDER: Cell<Option<NonNull<Yielder<(), UnwindReason>>>> = const { Cell::new(None) };
899    static TRAP_HANDLER: AtomicPtr<TrapHandlerContext> = const { AtomicPtr::new(ptr::null_mut()) };
900}
901
902/// Read-only information that is used by signal handlers to handle and recover
903/// from traps.
904#[allow(clippy::type_complexity)]
905struct TrapHandlerContext {
906    inner: *const u8,
907    handle_trap: fn(
908        *const u8,
909        usize,
910        usize,
911        Option<usize>,
912        Option<TrapCode>,
913        &mut dyn FnMut(TrapHandlerRegs),
914    ) -> bool,
915    custom_trap: Option<*const TrapHandlerFn<'static>>,
916}
917struct TrapHandlerContextInner<T> {
918    /// Information about the currently running coroutine. This is used to
919    /// reset execution to the root of the coroutine when a trap is handled.
920    coro_trap_handler: CoroutineTrapHandler<Result<T, UnwindReason>>,
921}
922
923impl TrapHandlerContext {
924    /// Runs the given function with a trap handler context. The previous
925    /// trap handler context is preserved and restored afterwards.
926    fn install<T, R>(
927        custom_trap: Option<*const TrapHandlerFn<'static>>,
928        coro_trap_handler: CoroutineTrapHandler<Result<T, UnwindReason>>,
929        f: impl FnOnce() -> R,
930    ) -> R {
931        // Type-erase the trap handler function so that it can be placed in TLS.
932        fn func<T>(
933            ptr: *const u8,
934            pc: usize,
935            sp: usize,
936            maybe_fault_address: Option<usize>,
937            trap_code: Option<TrapCode>,
938            update_regs: &mut dyn FnMut(TrapHandlerRegs),
939        ) -> bool {
940            unsafe {
941                (*(ptr as *const TrapHandlerContextInner<T>)).handle_trap(
942                    pc,
943                    sp,
944                    maybe_fault_address,
945                    trap_code,
946                    update_regs,
947                )
948            }
949        }
950        let inner = TrapHandlerContextInner { coro_trap_handler };
951        let ctx = Self {
952            inner: &inner as *const _ as *const u8,
953            handle_trap: func::<T>,
954            custom_trap,
955        };
956
957        compiler_fence(Ordering::Release);
958        let prev = TRAP_HANDLER.with(|ptr| {
959            let prev = ptr.load(Ordering::Relaxed);
960            ptr.store(&ctx as *const Self as *mut Self, Ordering::Relaxed);
961            prev
962        });
963
964        defer! {
965            TRAP_HANDLER.with(|ptr| ptr.store(prev, Ordering::Relaxed));
966            compiler_fence(Ordering::Acquire);
967        }
968
969        f()
970    }
971
972    /// Attempts to handle the trap if it's a wasm trap.
973    unsafe fn handle_trap(
974        pc: usize,
975        sp: usize,
976        maybe_fault_address: Option<usize>,
977        trap_code: Option<TrapCode>,
978        mut update_regs: impl FnMut(TrapHandlerRegs),
979        call_handler: impl Fn(&TrapHandlerFn<'static>) -> bool,
980    ) -> bool {
981        unsafe {
982            let ptr = TRAP_HANDLER.with(|ptr| ptr.load(Ordering::Relaxed));
983            if ptr.is_null() {
984                return false;
985            }
986
987            let ctx = &*ptr;
988
989            // Check if this trap is handled by a custom trap handler.
990            if let Some(trap_handler) = ctx.custom_trap
991                && call_handler(&*trap_handler)
992            {
993                return true;
994            }
995
996            (ctx.handle_trap)(
997                ctx.inner,
998                pc,
999                sp,
1000                maybe_fault_address,
1001                trap_code,
1002                &mut update_regs,
1003            )
1004        }
1005    }
1006}
1007
1008impl<T> TrapHandlerContextInner<T> {
1009    unsafe fn handle_trap(
1010        &self,
1011        pc: usize,
1012        sp: usize,
1013        maybe_fault_address: Option<usize>,
1014        trap_code: Option<TrapCode>,
1015        update_regs: &mut dyn FnMut(TrapHandlerRegs),
1016    ) -> bool {
1017        unsafe {
1018            // Check if this trap occurred while executing on the Wasm stack. We can
1019            // only recover from traps if that is the case.
1020            if !self.coro_trap_handler.stack_ptr_in_bounds(sp) {
1021                return false;
1022            }
1023
1024            let signal_trap = trap_code.or_else(|| {
1025                maybe_fault_address.map(|addr| {
1026                    if self.coro_trap_handler.stack_ptr_in_bounds(addr) {
1027                        TrapCode::StackOverflow
1028                    } else {
1029                        TrapCode::HeapAccessOutOfBounds
1030                    }
1031                })
1032            });
1033
1034            // Don't try to generate a backtrace for stack overflows: unwinding
1035            // information is often not precise enough to properly describe what is
1036            // happening during a function prologue, which can lead the unwinder to
1037            // read invalid memory addresses.
1038            //
1039            // See: https://github.com/rust-lang/backtrace-rs/pull/357
1040            let backtrace = if signal_trap == Some(TrapCode::StackOverflow) {
1041                Backtrace::from(vec![])
1042            } else {
1043                Backtrace::new_unresolved()
1044            };
1045
1046            // Set up the register state for exception return to force the
1047            // coroutine to return to its caller with UnwindReason::WasmTrap.
1048            let unwind = UnwindReason::WasmTrap {
1049                backtrace,
1050                signal_trap,
1051                pc,
1052            };
1053            let regs = self
1054                .coro_trap_handler
1055                .setup_trap_handler(move || Err(unwind));
1056            update_regs(regs);
1057            true
1058        }
1059    }
1060}
1061
1062unsafe fn unwind_with(reason: UnwindReason) -> ! {
1063    unsafe {
1064        let yielder = YIELDER
1065            .with(|cell| cell.replace(None))
1066            .expect("not running on Wasm stack");
1067
1068        yielder.as_ref().suspend(reason);
1069
1070        // on_wasm_stack will forcibly reset the coroutine stack after yielding.
1071        unreachable!();
1072    }
1073}
1074
1075/// Runs the given function on a separate stack so that its stack usage can be
1076/// bounded. Stack overflows and other traps can be caught and execution
1077/// returned to the root of the stack.
1078fn on_wasm_stack<F: FnOnce() -> T + 'static, T: 'static>(
1079    stack_size: usize,
1080    trap_handler: Option<*const TrapHandlerFn<'static>>,
1081    f: F,
1082) -> Result<T, UnwindReason> {
1083    // Reuse a cached stack — TLS first (atomic-free hot path), then the
1084    // cross-thread overflow pool, then allocate fresh. Size mismatches
1085    // (e.g. after `drain_stack_pool()` + a stack-size change) are filtered
1086    // inside `acquire_stack`. `base() - limit()` is the full mmap region
1087    // (including guard page), which is always >= the requested size for
1088    // stacks allocated with that size.
1089    let stack = acquire_stack(stack_size);
1090    let mut stack = scopeguard::guard(stack, release_stack);
1091
1092    // Create a coroutine with a new stack to run the function on.
1093    let coro = ScopedCoroutine::with_stack(&mut *stack, move |yielder, ()| {
1094        // Save the yielder to TLS so that it can be used later.
1095        YIELDER.with(|cell| cell.set(Some(yielder.into())));
1096
1097        Ok(f())
1098    });
1099
1100    // Ensure that YIELDER is reset on exit even if the coroutine panics,
1101    defer! {
1102        YIELDER.with(|cell| cell.set(None));
1103    }
1104
1105    coro.scope(|mut coro_ref| {
1106        // Set up metadata for the trap handler for the duration of the coroutine
1107        // execution. This is restored to its previous value afterwards.
1108        TrapHandlerContext::install(trap_handler, coro_ref.trap_handler(), || {
1109            match coro_ref.resume(()) {
1110                CoroutineResult::Yield(trap) => {
1111                    // This came from unwind_with which requires that there be only
1112                    // Wasm code on the stack.
1113                    unsafe {
1114                        coro_ref.force_reset();
1115                    }
1116                    Err(trap)
1117                }
1118                CoroutineResult::Return(result) => result,
1119            }
1120        })
1121    })
1122}
1123
1124/// When executing on the Wasm stack, temporarily switch back to the host stack
1125/// to perform an operation that should not be constrained by the Wasm stack
1126/// limits.
1127///
1128/// This is particularly important since the usage of the Wasm stack is under
1129/// the control of untrusted code. Malicious code could artificially induce a
1130/// stack overflow in the middle of a sensitive host operations (e.g. growing
1131/// a memory) which would be hard to recover from.
1132pub fn on_host_stack<F: FnOnce() -> T, T>(f: F) -> T {
1133    // Reset YIEDER to None for the duration of this call to indicate that we
1134    // are no longer on the Wasm stack.
1135    let yielder_ptr = YIELDER.with(|cell| cell.replace(None));
1136
1137    // If we are already on the host stack, execute the function directly. This
1138    // happens if a host function is called directly from the API.
1139    let yielder = match yielder_ptr {
1140        Some(ptr) => unsafe { ptr.as_ref() },
1141        None => return f(),
1142    };
1143
1144    // Restore YIELDER upon exiting normally or unwinding.
1145    defer! {
1146        YIELDER.with(|cell| cell.set(yielder_ptr));
1147    }
1148
1149    // on_parent_stack requires the closure to be Send so that the Yielder
1150    // cannot be called from the parent stack. This is not a problem for us
1151    // since we don't expose the Yielder.
1152    struct SendWrapper<T>(T);
1153    unsafe impl<T> Send for SendWrapper<T> {}
1154    let wrapped = SendWrapper(f);
1155    yielder.on_parent_stack(move || {
1156        let wrapped = wrapped;
1157        (wrapped.0)()
1158    })
1159}
1160
1161#[cfg(windows)]
1162pub fn lazy_per_thread_init() -> Result<(), Trap> {
1163    // We need additional space on the stack to handle stack overflow
1164    // exceptions. Rust's initialization code sets this to 0x5000 but this
1165    // seems to be insufficient in practice.
1166    use windows_sys::Win32::System::Threading::SetThreadStackGuarantee;
1167    if unsafe { SetThreadStackGuarantee(&mut 0x10000) } == 0 {
1168        panic!("failed to set thread stack guarantee");
1169    }
1170
1171    Ok(())
1172}
1173
1174/// A module for registering a custom alternate signal stack (sigaltstack).
1175///
1176/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not
1177/// always large enough for our signal handling code. Override it by creating
1178/// and registering our own alternate stack that is large enough and has a guard
1179/// page.
1180#[cfg(unix)]
1181pub fn lazy_per_thread_init() -> Result<(), Trap> {
1182    use std::ptr::null_mut;
1183
1184    thread_local! {
1185        /// Thread-local state is lazy-initialized on the first time it's used,
1186        /// and dropped when the thread exits.
1187        static TLS: Tls = unsafe { init_sigstack() };
1188    }
1189
1190    /// The size of the sigaltstack (not including the guard, which will be
1191    /// added). Make this large enough to run our signal handlers.
1192    const MIN_STACK_SIZE: usize = ByteSize::kib(64).as_u64() as usize;
1193
1194    enum Tls {
1195        OutOfMemory,
1196        Allocated {
1197            mmap_ptr: *mut libc::c_void,
1198            mmap_size: usize,
1199        },
1200        BigEnough,
1201    }
1202
1203    unsafe fn init_sigstack() -> Tls {
1204        unsafe {
1205            // Check to see if the existing sigaltstack, if it exists, is big
1206            // enough. If so we don't need to allocate our own.
1207            let mut old_stack = mem::zeroed();
1208            let r = libc::sigaltstack(ptr::null(), &mut old_stack);
1209            assert_eq!(r, 0, "learning about sigaltstack failed");
1210            if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE {
1211                return Tls::BigEnough;
1212            }
1213
1214            // ... but failing that we need to allocate our own, so do all that
1215            // here.
1216            let page_size: usize = region::page::size();
1217            let guard_size = page_size;
1218            let alloc_size = guard_size + MIN_STACK_SIZE;
1219
1220            let ptr = libc::mmap(
1221                null_mut(),
1222                alloc_size,
1223                libc::PROT_NONE,
1224                libc::MAP_PRIVATE | libc::MAP_ANON,
1225                -1,
1226                0,
1227            );
1228            if ptr == libc::MAP_FAILED {
1229                return Tls::OutOfMemory;
1230            }
1231
1232            // Prepare the stack with readable/writable memory and then register it
1233            // with `sigaltstack`.
1234            let stack_ptr = (ptr as usize + guard_size) as *mut libc::c_void;
1235            let r = libc::mprotect(
1236                stack_ptr,
1237                MIN_STACK_SIZE,
1238                libc::PROT_READ | libc::PROT_WRITE,
1239            );
1240            assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed");
1241            let new_stack = libc::stack_t {
1242                ss_sp: stack_ptr,
1243                ss_flags: 0,
1244                ss_size: MIN_STACK_SIZE,
1245            };
1246            let r = libc::sigaltstack(&new_stack, ptr::null_mut());
1247            assert_eq!(r, 0, "registering new sigaltstack failed");
1248
1249            Tls::Allocated {
1250                mmap_ptr: ptr,
1251                mmap_size: alloc_size,
1252            }
1253        }
1254    }
1255
1256    // Ensure TLS runs its initializer and return an error if it failed to
1257    // set up a separate stack for signal handlers.
1258    return TLS.with(|tls| {
1259        if let Tls::OutOfMemory = tls {
1260            Err(Trap::oom())
1261        } else {
1262            Ok(())
1263        }
1264    });
1265
1266    impl Drop for Tls {
1267        fn drop(&mut self) {
1268            let (ptr, size) = match self {
1269                Self::Allocated {
1270                    mmap_ptr,
1271                    mmap_size,
1272                } => (*mmap_ptr, *mmap_size),
1273                _ => return,
1274            };
1275            unsafe {
1276                // Deallocate the stack memory.
1277                let r = libc::munmap(ptr, size);
1278                debug_assert_eq!(r, 0, "munmap failed during thread shutdown");
1279            }
1280        }
1281    }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::*;
1287    use std::sync::Mutex;
1288
1289    // Guards tests that mutate global state (DEFAULT_STACK_SIZE, STACK_POOL).
1290    // Rust runs tests in parallel by default; this mutex serializes them so
1291    // they don't step on each other.
1292    static GLOBAL_STATE: Mutex<()> = Mutex::new(());
1293
1294    /// Saves the current stack size and restores it on drop (even on panic).
1295    struct RestoreStackSize(usize);
1296    impl Drop for RestoreStackSize {
1297        fn drop(&mut self) {
1298            set_stack_size(self.0);
1299        }
1300    }
1301
1302    #[test]
1303    fn max_stack_size_is_100mb() {
1304        assert_eq!(MAX_STACK_SIZE, ByteSize::mib(100).as_u64() as usize);
1305    }
1306
1307    #[test]
1308    fn get_set_stack_size_roundtrip() {
1309        let _lock = GLOBAL_STATE.lock().unwrap();
1310        let _restore = RestoreStackSize(get_stack_size());
1311        let new_size = ByteSize::mib(4).as_u64() as usize;
1312        set_stack_size(new_size);
1313        assert_eq!(get_stack_size(), new_size);
1314    }
1315
1316    #[test]
1317    fn set_stack_size_clamps_to_min() {
1318        let _lock = GLOBAL_STATE.lock().unwrap();
1319        let _restore = RestoreStackSize(get_stack_size());
1320        set_stack_size(1); // way below 8 KiB minimum
1321        assert_eq!(get_stack_size(), ByteSize::kib(8).as_u64() as usize);
1322    }
1323
1324    #[test]
1325    fn set_stack_size_clamps_to_max() {
1326        let _lock = GLOBAL_STATE.lock().unwrap();
1327        let _restore = RestoreStackSize(get_stack_size());
1328        set_stack_size(usize::MAX);
1329        assert_eq!(get_stack_size(), MAX_STACK_SIZE);
1330    }
1331
1332    #[test]
1333    fn drain_stack_pool_empties_pool() {
1334        let _lock = GLOBAL_STATE.lock().unwrap();
1335        let stack = DefaultStack::new(ByteSize::mib(1).as_u64() as usize).unwrap();
1336        STACK_POOL.push(stack);
1337        assert!(!STACK_POOL.is_empty());
1338        drain_stack_pool();
1339        assert!(STACK_POOL.is_empty());
1340    }
1341
1342    #[test]
1343    fn drain_stack_pool_is_idempotent() {
1344        let _lock = GLOBAL_STATE.lock().unwrap();
1345        drain_stack_pool();
1346        drain_stack_pool(); // second call on empty pool should not panic
1347        assert!(STACK_POOL.is_empty());
1348    }
1349
1350    /// The stack pool is not size-aware, so after a stack size increase it keeps
1351    /// serving cached undersized stacks. `drain_stack_pool()` breaks the cycle.
1352    ///
1353    /// 1. A call fills the pool with 500 KiB stacks (simulating normal execution).
1354    /// 2. The caller doubles the default to 1 MiB (simulating overflow retry).
1355    /// 3. WITHOUT draining, the pool still hands back a 500 KiB stack — the
1356    ///    retry would overflow again, creating an infinite loop.
1357    /// 4. After `drain_stack_pool()`, the pool is empty and the next allocation
1358    ///    must use the new, larger size.
1359    #[test]
1360    fn pool_returns_stale_stack_without_drain() {
1361        let _lock = GLOBAL_STATE.lock().unwrap();
1362        let _restore = RestoreStackSize(get_stack_size());
1363        drain_stack_pool();
1364
1365        // --- Phase 1: simulate normal execution that returns a 500 KiB stack ---
1366        let small_size = ByteSize::kib(500).as_u64() as usize;
1367        let small_stack = DefaultStack::new(small_size).unwrap();
1368        STACK_POOL.push(small_stack);
1369
1370        // --- Phase 2: "overflow detected" — caller doubles the default ---
1371        let big_size = ByteSize::mib(1).as_u64() as usize;
1372        set_stack_size(big_size);
1373        assert_eq!(get_stack_size(), big_size);
1374
1375        // --- Phase 3: WITHOUT drain, pool still returns the old small stack ---
1376        // This is the bug: the caller asked for a bigger stack but the pool
1377        // serves a cached undersized one, causing the retry to overflow again.
1378        let stale = STACK_POOL.pop();
1379        assert!(
1380            stale.is_some(),
1381            "pool should still contain the old stack (the bug scenario)"
1382        );
1383
1384        // --- Phase 4: with drain, pool is empty — next alloc uses new size ---
1385        STACK_POOL.push(stale.unwrap());
1386        drain_stack_pool();
1387        assert!(
1388            STACK_POOL.pop().is_none(),
1389            "after drain, pool must be empty so a fresh stack is allocated at the new size"
1390        );
1391    }
1392
1393    /// `on_wasm_stack` discards undersized stacks from the pool and allocates
1394    /// a fresh one instead of blindly reusing whatever the pool returns.
1395    #[test]
1396    fn on_wasm_stack_discards_undersized_stack() {
1397        let _lock = GLOBAL_STATE.lock().unwrap();
1398        let _restore = RestoreStackSize(get_stack_size());
1399        drain_stack_pool();
1400        clear_tls_stack();
1401
1402        // Push an undersized stack into the pool.
1403        let small_size = ByteSize::kib(500).as_u64() as usize;
1404        let small_stack = DefaultStack::new(small_size).unwrap();
1405        STACK_POOL.push(small_stack);
1406
1407        // Request a larger stack via on_wasm_stack.
1408        let big_size = ByteSize::mib(1).as_u64() as usize;
1409        let result = on_wasm_stack(big_size, None, || 42);
1410
1411        assert_eq!(result.expect("on_wasm_stack should succeed"), 42);
1412        // The undersized stack was discarded; the correctly-sized stack
1413        // allocated for the call now lives in the TLS cache (the hot path).
1414        // It will end up in the global pool only on thread exit or eviction.
1415        let returned = TLS_STACK
1416            .with(|cache| cache.0.take())
1417            .or_else(|| STACK_POOL.pop())
1418            .expect("stack should have been returned to TLS cache or pool");
1419        assert!(
1420            returned.size() >= big_size,
1421            "returned stack must be at least as large as the requested size"
1422        );
1423
1424        // Ensure no residual TLS state leaks into other tests sharing the
1425        // runner thread. `take()` above already cleared the slot, but be
1426        // explicit so future edits cannot drop this guarantee silently.
1427        clear_tls_stack();
1428    }
1429
1430    /// After a wasm call, the freshly-used stack stays in the thread-local
1431    /// cache so subsequent calls on the same thread reuse it without touching
1432    /// the global SegQueue.
1433    #[test]
1434    fn tls_stack_caches_after_first_call() {
1435        let _lock = GLOBAL_STATE.lock().unwrap();
1436        let _restore = RestoreStackSize(get_stack_size());
1437        drain_stack_pool();
1438        clear_tls_stack();
1439
1440        let size = get_stack_size();
1441
1442        // First call: TLS + pool both empty → allocate fresh; stack ends in TLS.
1443        assert!(on_wasm_stack(size, None, || ()).is_ok());
1444        assert!(
1445            STACK_POOL.is_empty(),
1446            "pool should still be empty after a TLS-served call"
1447        );
1448
1449        // Verify TLS holds a stack, then put it back.
1450        let cached_present = TLS_STACK.with(|cache| {
1451            let taken = cache.0.take();
1452            let present = taken.is_some();
1453            cache.0.set(taken);
1454            present
1455        });
1456        assert!(cached_present, "TLS slot should hold the post-call stack");
1457
1458        // Second call should consume from TLS; pool stays empty.
1459        assert!(on_wasm_stack(size, None, || ()).is_ok());
1460        assert!(
1461            STACK_POOL.is_empty(),
1462            "second call must not push to the global pool"
1463        );
1464        let still_cached = TLS_STACK.with(|cache| {
1465            let taken = cache.0.take();
1466            let present = taken.is_some();
1467            cache.0.set(taken);
1468            present
1469        });
1470        assert!(
1471            still_cached,
1472            "TLS slot should still hold a stack after the second call"
1473        );
1474
1475        // Cleanup: clear TLS so we don't leak into other tests. The cached
1476        // stack here is dropped rather than returned to the pool; the next
1477        // test starts with `drain_stack_pool()` anyway, so there is no
1478        // observable difference.
1479        clear_tls_stack();
1480    }
1481
1482    /// On thread exit, the TLS cache's `Drop` impl returns the held stack to
1483    /// the global pool so memory cycles correctly across thread lifetimes.
1484    #[test]
1485    fn tls_stack_returns_to_pool_on_thread_exit() {
1486        // GLOBAL_STATE is the test-suite mutex used to serialize tests that
1487        // touch shared global state (STACK_POOL, the configured stack size).
1488        // The spawned worker thread does NOT touch GLOBAL_STATE — it only
1489        // calls `on_wasm_stack`, which takes neither this mutex nor any
1490        // other lock that could contend with us.
1491        //
1492        // Even so, holding the guard across `handle.join()` is unnecessary:
1493        // the only thing that needs to be serialized against other tests is
1494        // the assertion on `STACK_POOL.pop()` AFTER the join. We release the
1495        // guard before joining so future edits to `on_wasm_stack` that
1496        // happen to touch this lock can't introduce a hard-to-debug
1497        // deadlock here.
1498        let lock = GLOBAL_STATE.lock().unwrap();
1499        let _restore = RestoreStackSize(get_stack_size());
1500        drain_stack_pool();
1501        clear_tls_stack();
1502
1503        let size = get_stack_size();
1504        drop(lock);
1505
1506        let handle = std::thread::spawn(move || {
1507            assert!(on_wasm_stack(size, None, || ()).is_ok());
1508        });
1509        handle.join().unwrap();
1510
1511        let _lock = GLOBAL_STATE.lock().unwrap();
1512        // The spawned thread's TLS cache was dropped on join; the stack must
1513        // have made it back to the global pool.
1514        let returned = STACK_POOL
1515            .pop()
1516            .expect("thread exit should return TLS-cached stack to the global pool");
1517        assert!(returned.size() >= size);
1518    }
1519
1520    // -----------------------------------------------------------------
1521    // Test helpers
1522    // -----------------------------------------------------------------
1523
1524    /// Clears the current thread's TLS slot so tests don't see state from
1525    /// previous tests (they share the same thread under cargo test's
1526    /// per-test serialization via `GLOBAL_STATE`).
1527    fn clear_tls_stack() {
1528        TLS_STACK.with(|cache| cache.0.set(None));
1529    }
1530
1531    /// `base().get() - limit().get()` (i.e. `Stack::size`) is constant per
1532    /// `DefaultStack` instance, but `base().get()` itself uniquely identifies
1533    /// the mmap allocation. We use it as a cheap identity check to see which
1534    /// stack was returned by acquire/release.
1535    fn stack_id(stack: &DefaultStack) -> usize {
1536        stack.base().get()
1537    }
1538
1539    // -----------------------------------------------------------------
1540    // acquire_stack mechanics
1541    // -----------------------------------------------------------------
1542
1543    #[test]
1544    fn acquire_allocates_fresh_when_tls_and_pool_empty() {
1545        let _lock = GLOBAL_STATE.lock().unwrap();
1546        let _restore = RestoreStackSize(get_stack_size());
1547        drain_stack_pool();
1548        clear_tls_stack();
1549
1550        let size = get_stack_size();
1551        let stack = acquire_stack(size);
1552        assert!(
1553            stack.size() >= size,
1554            "freshly allocated stack must satisfy min_size"
1555        );
1556
1557        drop(stack);
1558        clear_tls_stack();
1559        drain_stack_pool();
1560    }
1561
1562    #[test]
1563    fn acquire_prefers_tls_over_pool() {
1564        let _lock = GLOBAL_STATE.lock().unwrap();
1565        let _restore = RestoreStackSize(get_stack_size());
1566        drain_stack_pool();
1567        clear_tls_stack();
1568
1569        let size = get_stack_size();
1570        let tls_stack = DefaultStack::new(size).unwrap();
1571        let tls_id = stack_id(&tls_stack);
1572        TLS_STACK.with(|cache| cache.0.set(Some(tls_stack)));
1573
1574        let pool_stack = DefaultStack::new(size).unwrap();
1575        let pool_id = stack_id(&pool_stack);
1576        STACK_POOL.push(pool_stack);
1577
1578        let got = acquire_stack(size);
1579        assert_eq!(stack_id(&got), tls_id, "acquire must prefer TLS over pool");
1580        assert_ne!(stack_id(&got), pool_id);
1581
1582        drop(got);
1583        clear_tls_stack();
1584        drain_stack_pool();
1585    }
1586
1587    #[test]
1588    fn acquire_uses_pool_when_tls_empty() {
1589        let _lock = GLOBAL_STATE.lock().unwrap();
1590        let _restore = RestoreStackSize(get_stack_size());
1591        drain_stack_pool();
1592        clear_tls_stack();
1593
1594        let size = get_stack_size();
1595        let pool_stack = DefaultStack::new(size).unwrap();
1596        let pool_id = stack_id(&pool_stack);
1597        STACK_POOL.push(pool_stack);
1598
1599        let got = acquire_stack(size);
1600        assert_eq!(
1601            stack_id(&got),
1602            pool_id,
1603            "acquire must consume from pool when TLS is empty"
1604        );
1605        assert!(
1606            STACK_POOL.is_empty(),
1607            "pool stack must be removed when used"
1608        );
1609
1610        drop(got);
1611        clear_tls_stack();
1612        drain_stack_pool();
1613    }
1614
1615    #[test]
1616    fn acquire_discards_undersized_tls_then_allocates() {
1617        let _lock = GLOBAL_STATE.lock().unwrap();
1618        let _restore = RestoreStackSize(get_stack_size());
1619        drain_stack_pool();
1620        clear_tls_stack();
1621
1622        let small_size = ByteSize::kib(512).as_u64() as usize;
1623        let undersized = DefaultStack::new(small_size).unwrap();
1624        TLS_STACK.with(|cache| cache.0.set(Some(undersized)));
1625
1626        let big_size = ByteSize::mib(2).as_u64() as usize;
1627        let got = acquire_stack(big_size);
1628
1629        // The acquired stack must be at least the requested size. We do NOT
1630        // compare base addresses: the OS can reuse a freshly munmap'd
1631        // virtual address for the next mmap, so pointer identity is not a
1632        // reliable "is this a different stack" check across a drop+alloc.
1633        // The meaningful semantic is that the undersized stack was taken
1634        // out of rotation (TLS empty, not silently pushed to the pool) and
1635        // the returned stack is sized correctly.
1636        assert!(
1637            got.size() >= big_size,
1638            "acquired stack must satisfy big_size"
1639        );
1640        let tls_empty = TLS_STACK.with(|cache| {
1641            let s = cache.0.take();
1642            let empty = s.is_none();
1643            cache.0.set(s);
1644            empty
1645        });
1646        assert!(
1647            tls_empty,
1648            "undersized TLS stack must have been taken and discarded"
1649        );
1650        assert!(
1651            STACK_POOL.is_empty(),
1652            "undersized TLS stack must be discarded, not pushed to the pool",
1653        );
1654
1655        drop(got);
1656        clear_tls_stack();
1657        drain_stack_pool();
1658    }
1659
1660    #[test]
1661    fn acquire_discards_undersized_pool_then_allocates() {
1662        let _lock = GLOBAL_STATE.lock().unwrap();
1663        let _restore = RestoreStackSize(get_stack_size());
1664        drain_stack_pool();
1665        clear_tls_stack();
1666
1667        let small_size = ByteSize::kib(512).as_u64() as usize;
1668        let undersized = DefaultStack::new(small_size).unwrap();
1669        STACK_POOL.push(undersized);
1670
1671        let big_size = ByteSize::mib(2).as_u64() as usize;
1672        let got = acquire_stack(big_size);
1673
1674        // Same caveat as the TLS variant: mmap may reuse the virtual
1675        // address of the dropped undersized stack for the new big stack,
1676        // so we verify the semantic outcome — the pool was drained of the
1677        // undersized entry and the returned stack is sized correctly.
1678        assert!(
1679            got.size() >= big_size,
1680            "acquired stack must satisfy big_size"
1681        );
1682        assert!(
1683            STACK_POOL.is_empty(),
1684            "undersized pool stack must have been popped, filtered out and dropped",
1685        );
1686
1687        drop(got);
1688        clear_tls_stack();
1689        drain_stack_pool();
1690    }
1691
1692    // -----------------------------------------------------------------
1693    // release_stack mechanics
1694    // -----------------------------------------------------------------
1695
1696    #[test]
1697    fn release_into_empty_tls_caches_there() {
1698        let _lock = GLOBAL_STATE.lock().unwrap();
1699        drain_stack_pool();
1700        clear_tls_stack();
1701
1702        let size = get_stack_size();
1703        let stack = DefaultStack::new(size).unwrap();
1704        let id = stack_id(&stack);
1705        release_stack(stack);
1706
1707        let in_tls = TLS_STACK
1708            .with(|cache| cache.0.take())
1709            .expect("release into empty TLS should leave the stack in TLS");
1710        assert_eq!(stack_id(&in_tls), id);
1711        assert!(
1712            STACK_POOL.is_empty(),
1713            "pool must not be touched when TLS is empty"
1714        );
1715
1716        drain_stack_pool();
1717    }
1718
1719    #[test]
1720    fn release_into_occupied_tls_displaces_older_to_pool() {
1721        let _lock = GLOBAL_STATE.lock().unwrap();
1722        drain_stack_pool();
1723        clear_tls_stack();
1724
1725        let size = get_stack_size();
1726        let older = DefaultStack::new(size).unwrap();
1727        let older_id = stack_id(&older);
1728        TLS_STACK.with(|cache| cache.0.set(Some(older)));
1729
1730        let newer = DefaultStack::new(size).unwrap();
1731        let newer_id = stack_id(&newer);
1732        release_stack(newer);
1733
1734        let in_tls = TLS_STACK
1735            .with(|cache| cache.0.take())
1736            .expect("TLS should hold the newly-released stack");
1737        assert_eq!(
1738            stack_id(&in_tls),
1739            newer_id,
1740            "newer stack must displace into TLS"
1741        );
1742
1743        let displaced = STACK_POOL
1744            .pop()
1745            .expect("older stack should have been pushed to global pool");
1746        assert_eq!(
1747            stack_id(&displaced),
1748            older_id,
1749            "displaced stack must be the older one"
1750        );
1751
1752        drain_stack_pool();
1753    }
1754
1755    // -----------------------------------------------------------------
1756    // drain_stack_pool extended semantics
1757    // -----------------------------------------------------------------
1758
1759    #[test]
1760    fn drain_stack_pool_clears_calling_thread_tls_slot() {
1761        let _lock = GLOBAL_STATE.lock().unwrap();
1762        drain_stack_pool();
1763        clear_tls_stack();
1764
1765        let stack = DefaultStack::new(get_stack_size()).unwrap();
1766        TLS_STACK.with(|cache| cache.0.set(Some(stack)));
1767
1768        drain_stack_pool();
1769
1770        let tls_empty = TLS_STACK.with(|cache| cache.0.take().is_none());
1771        assert!(
1772            tls_empty,
1773            "drain_stack_pool must also clear current thread's TLS slot"
1774        );
1775        assert!(STACK_POOL.is_empty());
1776    }
1777
1778    // -----------------------------------------------------------------
1779    // on_wasm_stack functional behavior
1780    // -----------------------------------------------------------------
1781
1782    #[test]
1783    fn on_wasm_stack_passes_closure_value_back() {
1784        let _lock = GLOBAL_STATE.lock().unwrap();
1785        let _restore = RestoreStackSize(get_stack_size());
1786        drain_stack_pool();
1787        clear_tls_stack();
1788
1789        let r = on_wasm_stack(get_stack_size(), None, || 12345u32);
1790        assert_eq!(r.ok(), Some(12345));
1791
1792        clear_tls_stack();
1793        drain_stack_pool();
1794    }
1795
1796    #[test]
1797    fn on_wasm_stack_passes_owning_result_back() {
1798        let _lock = GLOBAL_STATE.lock().unwrap();
1799        let _restore = RestoreStackSize(get_stack_size());
1800        drain_stack_pool();
1801        clear_tls_stack();
1802
1803        // Use a heap-allocated value to verify the move-out path: the
1804        // closure produces a `Vec<u8>` that must travel from the coroutine
1805        // stack back to the host.
1806        let r = on_wasm_stack(get_stack_size(), None, || vec![0u8, 1, 2, 3, 4]);
1807        assert_eq!(r.ok(), Some(vec![0u8, 1, 2, 3, 4]));
1808
1809        clear_tls_stack();
1810        drain_stack_pool();
1811    }
1812
1813    #[test]
1814    fn many_calls_do_not_grow_global_pool() {
1815        let _lock = GLOBAL_STATE.lock().unwrap();
1816        let _restore = RestoreStackSize(get_stack_size());
1817        drain_stack_pool();
1818        clear_tls_stack();
1819
1820        // With TLS caching, repeated single-threaded calls must keep
1821        // reusing the same TLS stack and never push to the global pool.
1822        for _ in 0..1000 {
1823            assert!(on_wasm_stack(get_stack_size(), None, || ()).is_ok());
1824        }
1825        assert!(
1826            STACK_POOL.is_empty(),
1827            "1000 sequential calls should not grow the global pool (TLS handles reuse)"
1828        );
1829
1830        clear_tls_stack();
1831        drain_stack_pool();
1832    }
1833
1834    // -----------------------------------------------------------------
1835    // Trap and unwind paths
1836    // -----------------------------------------------------------------
1837
1838    #[test]
1839    fn raise_user_trap_yields_err() {
1840        let _lock = GLOBAL_STATE.lock().unwrap();
1841        let _restore = RestoreStackSize(get_stack_size());
1842        drain_stack_pool();
1843        clear_tls_stack();
1844
1845        let r: Result<(), UnwindReason> = on_wasm_stack(get_stack_size(), None, || unsafe {
1846            raise_user_trap(Box::new(io::Error::other("user trap from test")));
1847        });
1848        assert!(r.is_err(), "raise_user_trap must produce Err");
1849
1850        clear_tls_stack();
1851        drain_stack_pool();
1852    }
1853
1854    #[test]
1855    fn raise_lib_trap_yields_err() {
1856        let _lock = GLOBAL_STATE.lock().unwrap();
1857        let _restore = RestoreStackSize(get_stack_size());
1858        drain_stack_pool();
1859        clear_tls_stack();
1860
1861        let r: Result<(), UnwindReason> = on_wasm_stack(get_stack_size(), None, || unsafe {
1862            raise_lib_trap(Trap::lib(TrapCode::IntegerDivisionByZero));
1863        });
1864        assert!(r.is_err(), "raise_lib_trap must produce Err");
1865
1866        clear_tls_stack();
1867        drain_stack_pool();
1868    }
1869
1870    #[test]
1871    fn resume_panic_yields_err_without_unwinding() {
1872        // `resume_panic` packages the payload as `UnwindReason::Panic`. The
1873        // host-side panic resumption lives in `UnwindReason::into_trap`,
1874        // which we do NOT call here — `on_wasm_stack` itself just returns
1875        // the Err, so the test does not actually panic.
1876        let _lock = GLOBAL_STATE.lock().unwrap();
1877        let _restore = RestoreStackSize(get_stack_size());
1878        drain_stack_pool();
1879        clear_tls_stack();
1880
1881        let r: Result<(), UnwindReason> = on_wasm_stack(get_stack_size(), None, || unsafe {
1882            resume_panic(Box::new("panic payload from test"));
1883        });
1884        assert!(
1885            r.is_err(),
1886            "resume_panic must surface as Err to on_wasm_stack"
1887        );
1888
1889        clear_tls_stack();
1890        drain_stack_pool();
1891    }
1892
1893    #[test]
1894    fn trap_does_not_corrupt_subsequent_calls() {
1895        // After a trap, the per-call coroutine is force-reset and dropped.
1896        // The TLS stack cache and global pool must remain in a usable state
1897        // so that subsequent calls succeed.
1898        let _lock = GLOBAL_STATE.lock().unwrap();
1899        let _restore = RestoreStackSize(get_stack_size());
1900        drain_stack_pool();
1901        clear_tls_stack();
1902
1903        let trapped: Result<(), UnwindReason> = on_wasm_stack(get_stack_size(), None, || unsafe {
1904            raise_user_trap(Box::new(io::Error::other("first call traps")));
1905        });
1906        assert!(trapped.is_err());
1907
1908        // Subsequent normal call must still succeed.
1909        let ok = on_wasm_stack(get_stack_size(), None, || 7u32);
1910        assert_eq!(ok.ok(), Some(7), "calls after a trap must still work");
1911
1912        clear_tls_stack();
1913        drain_stack_pool();
1914    }
1915
1916    // -----------------------------------------------------------------
1917    // on_host_stack
1918    // -----------------------------------------------------------------
1919
1920    #[test]
1921    fn on_host_stack_outside_coroutine_runs_inline() {
1922        // `on_host_stack` outside any wasm coroutine just runs `f()` directly
1923        // (no stack switch); this asserts the no-yielder branch still works.
1924        let _lock = GLOBAL_STATE.lock().unwrap();
1925        let n = on_host_stack(|| 99i32);
1926        assert_eq!(n, 99);
1927    }
1928
1929    #[test]
1930    fn on_host_stack_inside_wasm_switches_and_returns() {
1931        let _lock = GLOBAL_STATE.lock().unwrap();
1932        let _restore = RestoreStackSize(get_stack_size());
1933        drain_stack_pool();
1934        clear_tls_stack();
1935
1936        let r = on_wasm_stack(get_stack_size(), None, || on_host_stack(|| 88i32));
1937        assert_eq!(r.ok(), Some(88));
1938
1939        clear_tls_stack();
1940        drain_stack_pool();
1941    }
1942
1943    // -----------------------------------------------------------------
1944    // Re-entrancy
1945    // -----------------------------------------------------------------
1946
1947    #[test]
1948    fn reentrant_call_returns_value() {
1949        let _lock = GLOBAL_STATE.lock().unwrap();
1950        let _restore = RestoreStackSize(get_stack_size());
1951        drain_stack_pool();
1952        clear_tls_stack();
1953
1954        let outer = on_wasm_stack(get_stack_size(), None, || {
1955            on_wasm_stack(get_stack_size(), None, || 42i32).expect("inner must succeed")
1956        });
1957        assert_eq!(outer.ok(), Some(42));
1958
1959        clear_tls_stack();
1960        drain_stack_pool();
1961    }
1962
1963    #[test]
1964    fn reentrant_calls_run_to_completion_under_pool_pressure() {
1965        // The outer call's stack is held by its scopeguard for the duration
1966        // of the call; the inner call must therefore pop a separate stack
1967        // from the pool (or allocate one). With a pre-populated pool the
1968        // inner call should consume that stack; either way the nested call
1969        // chain must complete without deadlocking or panicking from
1970        // corosensei.
1971        use std::sync::Arc;
1972        use std::sync::atomic::{AtomicUsize, Ordering as O};
1973
1974        let _lock = GLOBAL_STATE.lock().unwrap();
1975        let _restore = RestoreStackSize(get_stack_size());
1976        drain_stack_pool();
1977        clear_tls_stack();
1978
1979        // Pre-populate the pool with one stack so the inner call can grab
1980        // it instead of allocating.
1981        let pre = DefaultStack::new(get_stack_size()).unwrap();
1982        STACK_POOL.push(pre);
1983
1984        let inner_completed = Arc::new(AtomicUsize::new(0));
1985        let inner_completed_outer = inner_completed.clone();
1986        let _ = on_wasm_stack(get_stack_size(), None, move || {
1987            let inner_completed = inner_completed_outer.clone();
1988            let inner = on_wasm_stack(get_stack_size(), None, move || {
1989                inner_completed.fetch_add(1, O::Relaxed);
1990            });
1991            assert!(inner.is_ok(), "inner re-entrant call must succeed");
1992        });
1993        assert_eq!(
1994            inner_completed.load(O::Relaxed),
1995            1,
1996            "inner closure must have executed exactly once",
1997        );
1998
1999        clear_tls_stack();
2000        drain_stack_pool();
2001    }
2002
2003    #[test]
2004    fn reentrant_inner_trap_does_not_kill_outer() {
2005        let _lock = GLOBAL_STATE.lock().unwrap();
2006        let _restore = RestoreStackSize(get_stack_size());
2007        drain_stack_pool();
2008        clear_tls_stack();
2009
2010        let outer = on_wasm_stack(get_stack_size(), None, || {
2011            let inner: Result<i32, UnwindReason> =
2012                on_wasm_stack(get_stack_size(), None, || unsafe {
2013                    raise_user_trap(Box::new(io::Error::other("inner trap")));
2014                });
2015            // Outer observes inner's Err and recovers.
2016            match inner {
2017                Err(_) => 1234i32,
2018                Ok(_) => panic!("inner should have trapped"),
2019            }
2020        });
2021        assert_eq!(
2022            outer.ok(),
2023            Some(1234),
2024            "outer must recover after inner trap and run to completion"
2025        );
2026
2027        clear_tls_stack();
2028        drain_stack_pool();
2029    }
2030
2031    #[test]
2032    fn reentrant_with_on_host_stack_in_between() {
2033        // Outer wasm → on_host_stack → inner wasm. This exercises the
2034        // YIELDER save/restore in `unwind_with` / `on_host_stack` against
2035        // a re-entrant boundary.
2036        let _lock = GLOBAL_STATE.lock().unwrap();
2037        let _restore = RestoreStackSize(get_stack_size());
2038        drain_stack_pool();
2039        clear_tls_stack();
2040
2041        let r = on_wasm_stack(get_stack_size(), None, || {
2042            on_host_stack(|| {
2043                on_wasm_stack(get_stack_size(), None, || 5i32).expect("nested inner must succeed")
2044            })
2045        });
2046        assert_eq!(r.ok(), Some(5));
2047
2048        clear_tls_stack();
2049        drain_stack_pool();
2050    }
2051
2052    // -----------------------------------------------------------------
2053    // Concurrency
2054    // -----------------------------------------------------------------
2055
2056    #[test]
2057    fn many_threads_in_parallel_all_succeed() {
2058        let _lock = GLOBAL_STATE.lock().unwrap();
2059        let _restore = RestoreStackSize(get_stack_size());
2060        drain_stack_pool();
2061
2062        use std::sync::Arc;
2063        use std::sync::atomic::{AtomicUsize, Ordering as O};
2064
2065        let counter = Arc::new(AtomicUsize::new(0));
2066        const THREADS: usize = 8;
2067        const CALLS_PER_THREAD: usize = 200;
2068
2069        let handles: Vec<_> = (0..THREADS)
2070            .map(|_| {
2071                let counter = counter.clone();
2072                std::thread::spawn(move || {
2073                    let size = get_stack_size();
2074                    for _ in 0..CALLS_PER_THREAD {
2075                        if on_wasm_stack(size, None, || 1u32).ok() == Some(1) {
2076                            counter.fetch_add(1, O::Relaxed);
2077                        }
2078                    }
2079                })
2080            })
2081            .collect();
2082        for h in handles {
2083            h.join().unwrap();
2084        }
2085
2086        assert_eq!(counter.load(O::Relaxed), THREADS * CALLS_PER_THREAD);
2087        // Pool should now hold at most `THREADS` stacks (one per thread that
2088        // exited). Each thread also drops its TLS slot on exit, which pushes
2089        // the stack to the pool.
2090        let mut pooled = 0usize;
2091        while STACK_POOL.pop().is_some() {
2092            pooled += 1;
2093        }
2094        assert!(
2095            pooled <= THREADS,
2096            "pool should hold at most one stack per terminated thread (got {pooled} for {THREADS} threads)"
2097        );
2098
2099        clear_tls_stack();
2100        drain_stack_pool();
2101    }
2102
2103    // -----------------------------------------------------------------
2104    // Stack size dynamics
2105    // -----------------------------------------------------------------
2106
2107    #[test]
2108    fn growing_request_discards_smaller_tls_stack() {
2109        let _lock = GLOBAL_STATE.lock().unwrap();
2110        let _restore = RestoreStackSize(get_stack_size());
2111        drain_stack_pool();
2112        clear_tls_stack();
2113
2114        // First call at a small size populates TLS with a small stack.
2115        let small = ByteSize::mib(1).as_u64() as usize;
2116        set_stack_size(small);
2117        assert!(on_wasm_stack(small, None, || ()).is_ok());
2118
2119        let cached_size = TLS_STACK.with(|cache| {
2120            let s = cache.0.take();
2121            let sz = s.as_ref().map_or(0, |s| s.size());
2122            cache.0.set(s);
2123            sz
2124        });
2125        assert!(
2126            cached_size >= small,
2127            "TLS should hold the small-sized stack"
2128        );
2129
2130        // Now request a larger stack. acquire_stack should discard the TLS
2131        // entry and either pop a big-enough one from pool or allocate.
2132        let big = ByteSize::mib(4).as_u64() as usize;
2133        set_stack_size(big);
2134        assert!(on_wasm_stack(big, None, || ()).is_ok());
2135
2136        // The TLS slot should now hold a stack that's big enough.
2137        let cached_size = TLS_STACK.with(|cache| {
2138            let s = cache.0.take();
2139            let sz = s.as_ref().map_or(0, |s| s.size());
2140            cache.0.set(s);
2141            sz
2142        });
2143        assert!(
2144            cached_size >= big,
2145            "TLS should hold the bigger stack after size bump"
2146        );
2147
2148        clear_tls_stack();
2149        drain_stack_pool();
2150    }
2151}