Signal Handling Internals

2026-04-23

You already know that signal() and sigaction() register handlers, but what actually happens inside the kernel when a signal is delivered? Understanding the machinery matters because subtle bugs — corrupt state, deadlocks in handlers, missed signals — all stem from this mechanism.

The delivery path. When the kernel decides to deliver a signal (returning from a syscall, interrupt, or context switch), it doesn't call your handler directly. Instead it manipulates the target thread's user-space stack:

This means your handler runs in the same thread, on top of whatever that thread was doing. If the thread was halfway through malloc() and you call malloc() in your handler, you corrupt the heap. This is why only async-signal-safe functions (POSIX defines ~30 of them, like write(), _exit(), read()) are legal inside handlers.

Real-world example: crash handlers. Programs like Chrome and Firefox install SIGSEGV/SIGABRT handlers that write a minidump to disk. They use sigaltstack() to allocate a dedicated 8–16 KB signal stack because the default stack may be the one that overflowed. Inside the handler they avoid printf or malloc — they write raw bytes with the write() syscall.

The sigaltstack rule of thumb: allocate at least SIGSTKSZ (typically 8192 bytes on x86-64 Linux) for your alternate signal stack. If your handler does more work (e.g., stack unwinding for backtraces), use MINSIGSTKSZ + your_estimate. A safe default is 32 KB.

Signal masks and atomicity. Each thread has a signal mask — a bitmask of blocked signals. While your handler runs, the delivered signal is automatically masked (and any signals listed in sa_mask from sigaction). This prevents re-entrant delivery of the same signal. You can inspect the mask with sigprocmask() (process-wide) or pthread_sigmask() (per-thread).

Practical pattern: the self-pipe trick. Since handlers can't safely do complex work, a common pattern is: the handler writes a single byte to a pipe with write(fd, &byte, 1), and your main event loop polls that pipe with select()/epoll(). Modern Linux offers signalfd() which eliminates the handler entirely — signals arrive as readable events on a file descriptor. Prefer signalfd() when portability to non-Linux isn't required.

See it in action: Check out How to deeply understand Angular signals (...or anything) by Joshua Morony to see this theory applied.
Key Takeaway: Signals are delivered by the kernel rewriting your thread's stack and instruction pointer, so your handler runs on borrowed context — keep it minimal, async-signal-safe, and use sigaltstack() for crash handlers.

All newsletters