The Physics of Signals: Stack Injection & Re-entrancy
Why `printf` inside a signal handler causes deadlocks. The physics of Userspace Stack Injection, the Trampoline, and Async-Signal-Safety.
🎯 What You'll Learn
- Trace the kernel signal injection mechanism (Stack Hacking)
- Understand the `malloc` deadlock (Async-Signal-Safety)
- Explain the `sigreturn` Trampoline
- Master proper signal masking with `sigprocmask`
- Differentiate Re-entrant vs Standard functions
📚 Prerequisites
Before this lesson, you should understand:
Introduction
Signals are often called “Software Interrupts,” but that description is too gentle. A Signal is Kernel-Level Code Injection.
When you send SIGINT (Ctrl+C) to a process, the kernel effectively pauses your application, manually rewrites its stack memory, and forces the CPU to jump to a new function.
This violent interruption is why writing signal handlers is one of the most dangerous tasks in systems programming.
This lesson explores the physics of this injection-and why one wrong function call can hang your server forever.
The Physics: How a Signal is Delivered
You might think the Kernel just “calls” your handler function. It can’t. Your handler is in User Space. The Kernel is in Kernel Space. The CPU cannot just jump between them arbitrarily.
The Injection Sequence
When the Kernel decides to deliver a signal to your process (e.g., returning from a syscall):
- Pause: The Kernel pauses your process execution.
- Hack the Stack: The Kernel writes a new “Signal Frame” onto your process’s User Stack.
- This frame contains the CPU registers (to restore later).
- Push the Trampoline: The Kernel pushes a return address that points to
sigreturn(a magic snippet of assembly). - Force Jump: The Kernel modifies the Instruction Pointer (
RIP) to point to your Handler Function. - Resume: The process resumes in User Mode, executing the handler.
Physics Note: Your process literally “wakes up” in a different function than where it fell asleep.
The Danger: Async-Signal-Safety
Because a signal can strike at any instruction boundary, your handler runs in a stolen context.
Imagine your main thread is inside malloc(). It has acquired the global memory lock.
Suddenly, a signal fires.
Your handler runs. Inside the handler, you call printf() (which calls malloc() internally).
- Main Thread: Holds execution of
malloc(Lock held). - Signal Interrupts Main Thread.
- Signal Handler calls
malloc. malloctries to acquire the lock.- DEADLOCK. The thread holding the lock is the thread waiting for the lock.
The Golden Rule
NEVER call complex standard library functions in a signal handler.
NO malloc, printf, free, exit.
YES write, _exit, sigaction.
These are “Async-Signal-Safe.” They are guaranteed to be re-entrant or atomic.
Code: The Re-entrant Handler
How do you handle signals safely? Two ways.
1. The Atomic Flag
Do almost nothing. Just set a global flag and return.
// volatile is crucial: tells compiler "this value changes unexpectedly"
// sig_atomic_t is crucial: guarantees the write happens in one CPU cycle
volatile sig_atomic_t stop_requested = 0;
void handler(int signum) {
stop_requested = 1;
}
int main() {
signal(SIGTERM, handler);
while (!stop_requested) {
// Do heavy work
}
// Cleanup here, safely outside the handler
}
2. The Self-Pipe Trick
If you need to wake up an event loop (like epoll):
- Create a pipe (
pipe()). - In the signal handler,
write(pipe_fd, "x", 1). - In your main loop,
epollmonitors the pipe’s read end. - When the loop wakes up, read the byte and handle the signal safely.
Deep Dive: The Sigreturn Trampoline
When your handler function finishes (return), where does it go?
It cannot go back to main(). It has to tell the kernel: “I’m done, please restore the registers you saved.”
This is the job of the Trampoline.
The kernel injected a small piece of assembly code onto your stack.
When your handler returns, it pops this address and jumps to it.
This code calls the sys_rt_sigreturn syscall.
Physics: User App -> (Signal) -> Kernel -> (Inject Frame) -> User Handler -> (Return) -> Trampoline -> (Syscall) -> Kernel -> (Restore Registers) -> User App.
Common Signals for SREs
| Signal | ID | Physics | Can Catch? |
|---|---|---|---|
SIGINT | 2 | Keyboard Interrupt (Ctrl+C) | Yes |
SIGKILL | 9 | Immediate Process Destruction | NO |
SIGSEGV | 11 | Invalid Memory Access (Hardware Fault) | Yes (but risky) |
SIGTERM | 15 | Polite Request to Die | Yes |
SIGSTOP | 19 | Pause Execution (Scheduler remove) | NO |
Why SIGKILL is bad: SIGKILL does not give the process a chance to clean up. Database files get corrupted. Temp files are left behind. Always use SIGTERM first.
Practice Exercises
Exercise 1: The Deadlock (Intermediate)
Task: Write a C program that calls malloc() in an infinite loop. Register a signal handler that also calls printf() (which locks). Flood it with signals.
Result: It will eventually hang.
Exercise 2: The Mask (Advanced)
Task: Use sigprocmask() to block SIGINT.
Action: Press Ctrl+C. Nothing happens.
Action: Unblock it. The pending signal is immediately delivered. Physics: The kernel queued the signal in the task_struct.
Exercise 3: The Crash Handler (Expert)
Task: Catch SIGSEGV.
Action: In the handler, print “Oops” using write(1, ...) and then _exit(1).
Why: This allows you to log a crash before dying, unlike the default behavior.
Knowledge Check
- Which stack does the signal handler run on?
- Why is
printfunsafe in a handler? - What is the purpose of
volatile sig_atomic_t? - How does the process return to the original code after the handler?
- Can you catch
SIGKILL?
Answers
- The User Stack. (Unless a dedicated
sigaltstackis configured). - Internal Locks. It is not re-entrant. Deadlock risk.
- Atomicity & Visibility. Guarantees 1-cycle write and prevents compiler caching.
- The Trampoline. Calls
sys_rt_sigreturnto restore context. - No. The kernel kills the process immediately before checking for handlers.
Summary
- Signals: Kernel Code Injection.
- Safety: Do almost nothing in a handler.
- Trampoline: The path back to sanity.
- SIGKILL: The nuclear option.
Questions about this lesson? Working on related infrastructure?
Let's discuss