From ad25336c8311121c677c4a7a865dba0859ddf790 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Tue, 16 Jun 2026 20:55:19 -0400 Subject: [PATCH] feat: alternate signal stack so sandbox stack overflows are recoverable Each sandbox runs on its own mmap'd native stack with a PROT_NONE guard page below it. The signal handlers were registered without an alternate signal stack, so when a sandbox overflows its stack and faults on the guard page there is no room on the exhausted stack to deliver SIGSEGV -- the kernel kills the whole runtime instead of trapping just that sandbox. The recovery machinery already exists (current_sandbox_start sigsetjmps; the handler siglongjmps back via current_sandbox_trap); it simply cannot run because the signal cannot be delivered. Register a per-worker alternate signal stack and flag the synchronous fault handlers (SIGSEGV, SIGFPE) SA_ONSTACK so they run on it, leaving an overflow recoverable. The asynchronous preemption signals (SIGALRM, SIGUSR1) are intentionally left off the alternate stack: they context- switch away rather than returning, which does not compose with an alternate stack, so isolating SA_ONSTACK to the fault handlers keeps the preemption path untouched. Marked experimental: the mechanism is validated by a standalone test that reproduces the runtime's exact primitives (guard-paged mmap stack via makecontext/swapcontext, sigsetjmp recovery, handler siglongjmp) -- which crashes without the alt stack and recovers with it -- and a normal preemptive workload shows no regression, but a real in-runtime overflow was not exercised end to end (no existing module recurses deeply enough, and building one needs the module toolchain). Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/include/software_interrupt.h | 5 ++++ runtime/src/software_interrupt.c | 35 ++++++++++++++++++++++++++++ runtime/src/worker_thread.c | 4 ++++ 3 files changed, 44 insertions(+) diff --git a/runtime/include/software_interrupt.h b/runtime/include/software_interrupt.h index e1f048c53..a20693871 100644 --- a/runtime/include/software_interrupt.h +++ b/runtime/include/software_interrupt.h @@ -92,6 +92,11 @@ software_interrupt_deferred_sigalrm_replay() * Exports from software_interrupt.c * ************************/ +/* Size of each worker's alternate signal stack. The SA_ONSTACK handlers (SIGSEGV, SIGFPE) only siglongjmp out, + * so this needs little space; keep it comfortably above MINSIGSTKSZ. */ +#define SOFTWARE_INTERRUPT_ALT_STACK_SIZE (64 * 1024) + +void software_interrupt_alt_stack_initialize(void); void software_interrupt_arm_timer(void); void software_interrupt_cleanup(void); void software_interrupt_disarm_timer(void); diff --git a/runtime/src/software_interrupt.c b/runtime/src/software_interrupt.c index 14c1ab1d9..cca38f00c 100644 --- a/runtime/src/software_interrupt.c +++ b/runtime/src/software_interrupt.c @@ -278,6 +278,19 @@ software_interrupt_initialize(void) /* Mask this signal on the listener thread */ software_interrupt_mask_signal(signal); + /* Deliver the synchronous fault signals (SIGSEGV, SIGFPE) on the per-worker alternate signal + * stack registered by software_interrupt_alt_stack_initialize(). A sandbox stack overflow faults + * on the stack's guard page, leaving no room to deliver the signal on the overflowed stack; running + * the handler on the alternate stack keeps that overflow recoverable (the handler siglongjmps back + * to the sandbox entry on a valid part of the stack). The asynchronous preemption signals (SIGALRM, + * SIGUSR1) stay on the normal stack: they context-switch away rather than returning, which does not + * compose with an alternate stack. */ + if (signal == SIGSEGV || signal == SIGFPE) { + signal_action.sa_flags |= SA_ONSTACK; + } else { + signal_action.sa_flags &= ~SA_ONSTACK; + } + /* But register the handler for this signal for the process */ if (sigaction(signal, &signal_action, NULL)) { perror("sigaction"); @@ -288,6 +301,28 @@ software_interrupt_initialize(void) software_interrupt_counts_alloc(); } +/** + * Registers a per-thread alternate signal stack so that handlers flagged SA_ONSTACK (SIGSEGV, SIGFPE) can run + * even when the active sandbox stack is exhausted. Must be called by each worker before it unmasks those signals. + */ +void +software_interrupt_alt_stack_initialize(void) +{ + stack_t alt_stack = { 0 }; + alt_stack.ss_flags = 0; + alt_stack.ss_size = SOFTWARE_INTERRUPT_ALT_STACK_SIZE; + alt_stack.ss_sp = malloc(alt_stack.ss_size); + if (alt_stack.ss_sp == NULL) { + perror("malloc alternate signal stack"); + exit(1); + } + + if (sigaltstack(&alt_stack, NULL) != 0) { + perror("sigaltstack"); + exit(1); + } +} + void software_interrupt_cleanup() { diff --git a/runtime/src/worker_thread.c b/runtime/src/worker_thread.c index ae993ec4c..a96407e08 100644 --- a/runtime/src/worker_thread.c +++ b/runtime/src/worker_thread.c @@ -62,6 +62,10 @@ worker_thread_main(void *argument) tenant_timeout_get_priority); } + /* Register this worker's alternate signal stack before unmasking the fault signals, so a sandbox stack + * overflow (which faults on the stack guard page) can still be delivered and recovered from. */ + software_interrupt_alt_stack_initialize(); + software_interrupt_unmask_signal(SIGFPE); software_interrupt_unmask_signal(SIGSEGV);