Wrapping Signal Handlers
We want to wrap signal handlers with a glibc function, so that we can perform the following actions around a signal handler:
- Save, set, and restore a thread-local flag which indicates that the current thread is executing a signal handler. This has several applications:
In fork, skip execution of glibc-internal fork handlers (such as those for malloc) when in a signal handler. These fork handlers are not needed because the child process is only allowed to call async-signal-safe functions.
In the internal malloc, use mmap unconditionally if executing in a signal handler (to simplify implementation of a async-signal-safe syslog, sprintf, and dprintf functions, as commonly required by applications).
- Implement an audit module which checks if signal handlers call only async-signal-safe functions.
Save and restore errno. Very few applications handle errno in a signal handler correctly, and this change would fix spurious race conditions.
Desired signal behavior
Also see POSIX Signal Concepts.
State
For each signal, but globally for the entire process: The handler address (function pointer) and the signal flags (the sa_flags value passed to sigaction).
For each signal and thread: A Boolean flag which indicates if the signal is blocked for that thread (the signal mask).
For each signal and thread: A queue of pending signals.
A signal is generated by some event (either by a hardware event, or an explicit signal generation via a call to functions such as kill and pthread_kill) and delivered (when the signal handler is executed). If a signal is blocked for a thread and cannot be delivered to any other thread, it is queued and enters the pending state.
Invariants
- If a signal is delivered to a thread, it is not blocked for that thread.
When a signal is delivered to a thread, the handler and signal flags correspond to a previous invocation of the sigaction function which installed a signal handler.
The handler and signal flags have been specified in the same call to sigaction.
If sigaction is called multiple while the signal is pending, the last call to sigaction before the signal is unblocked determines the handler and flags when the signal is delivered.
Current implementation
The kernel has a mechanism to queue signals when they need to be delivered to a particular thread, but the signal number is blocked for that thread.
When delivering the signal, the kernel reads the signal flags and the handler atomically with regards to sigaction (that is, sigaction changes both values atomically).
Challenges
- Debuggers and the unwinder expect that signal handler frames are marked in a specific way.
The kernel only provides the signal number to the signal handler. There is no closure pointer (like in qsort_r or pthread_create).
- If we maintain our own signal handler table in user space (with a function pointer for each signal number), the application signal handler and the signal handler flags may not be read in an atomic fashion, and the difference could be observable to the application.
- It is very costly to determine if the kernel is about to deliver a signal or has already started to execute a signal handler (including the signal handler wrapper function). It would be required to send a signal to all threads and look at the PC value, to determine whether it is within the range of the signal handler.
Things that do not work
- A simple user-space signal handler table which is accessed in the glibc-provided signal handler wrapper to find the actual handler registered by the application does not work. The reason is that the kernel loads the signal handler flags and calls the wrapper, and the wrapper loads the actual handler address, and this is no longer atomic. Blocking signals does not change that: the signal handler wrapper might already be running. The race window starts before glibc code runs, so there does not seem to be a way to address this issue as long as a simple signal/handler table (with one handler address for each signal) is used in user space.
Potential kernel changes
- We could add a mechanism to specify the TLS pointer for a signal when setting up the signal handler, and point it somewhere distinct, so that the signal handler gets its own errno and similar.