2026-07-08
When a signal fires, the kernel pushes a signal frame onto the current thread's stack and jumps to your handler. This creates a chicken-and-egg problem: what if the signal is a stack overflow? The kernel tries to push the frame, hits the guard page, delivers SIGSEGV for the frame push itself, and your process dies without your handler ever running. The sigaltstack() syscall solves this by letting you register a separate memory region that the kernel will use instead — but only for signals whose handler was installed with SA_ONSTACK.
The mechanics: you allocate a buffer (at least SIGSTKSZ, typically 8 KB but ideally 16-64 KB on modern glibc), call sigaltstack(&ss, NULL) with ss.ss_sp pointing to it, then install your handler with sa.sa_flags |= SA_ONSTACK. When the signal fires, the kernel checks whether the current SP is already inside the alt stack (to avoid nesting), and if not, switches RSP to ss_sp + ss_size before building the frame.
Real-world example: The JVM uses this for every thread. Java's stack overflow detection depends on catching SIGSEGV from the guard page, but the handler that turns it into a StackOverflowError can't run on the overflowed stack. HotSpot allocates a per-thread alt stack (~32 KB) at thread creation. Go's runtime does the same — every goroutine's M has an alt stack so the runtime can recover from segfaults in cgo callbacks that blew their C stack. Rust's backtrace crate configures one when installing panic handlers that want to survive stack exhaustion.
The subtle gotcha: the alt stack is per-thread, not per-process. Each thread you create must call sigaltstack() itself, or its signals fall back to the regular stack. Pthreads doesn't do this for you. Miss it, and stack overflows in worker threads become silent process death while your main thread's handler works fine — a classic "works in tests, dies in prod" bug.
Sizing rule of thumb: your alt stack must fit the signal frame (~1 KB on x86-64 with XSAVE state including AVX-512: 2560 bytes just for FPU state) plus whatever your handler does. Handlers that call backtrace() and format strings easily consume 8 KB. Use SIGSTKSZ as a floor, but budget at least 16 KB in practice. Since Linux 5.11, SIGSTKSZ is dynamic and reflects the actual XSAVE area size — hardcoding the old 8192 constant will silently truncate frames on machines with AMX enabled.
