Signal Handler Function Associated with Multiple Signals
Description
Signal Handler Function Associated with Multiple Signals is a concurrency vulnerability where a single function is registered as the handler for multiple different signals. While this approach may seem efficient, it creates potential security issues when the handler uses shared state, calls non-reentrant functions, or has side effects. When a handler is executing in response to one signal, another signal associated with the same handler can interrupt it, causing the handler to be invoked recursively. This can lead to race conditions, state corruption, double-free vulnerabilities, or other undefined behaviors that attackers can exploit.
Risk
When a handler processes one signal and is interrupted by another signal that triggers the same handler, the shared state and resources become corrupted. Global variables may be left in inconsistent states. Memory that was freed during the first invocation may be freed again (double-free). Non-reentrant functions like malloc() or syslog() may corrupt their internal data structures when called recursively. Attackers who can send multiple signals to a process can time their attacks to trigger these race conditions, potentially achieving denial of service or code execution. The vulnerability is particularly dangerous because the same handler function processes both signal invocations.
Solution
Use separate handler functions for different signals, each handling its specific concerns. If multiple signals must share logic, extract it into a helper function that the handlers call, but ensure the helper is async-signal-safe and reentrant. Block related signals while a handler executes using sigprocmask() or signal masks. Design handlers to only set a flag that the main program loop checks, deferring actual processing to a safe context. Use the self-pipe trick or signalfd() to convert signals into file descriptor events that can be handled in a normal execution context. Avoid using non-reentrant functions in any signal handler.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Crash, Exit, or Restart - State corruption from reentrant handler invocations causes crashes or hangs. |
| Integrity, Confidentiality, Availability | Scope: Integrity, Confidentiality, Availability Execute Unauthorized Code or Commands - Corruption of heap metadata or function pointers may enable code execution. |
| Confidentiality | Scope: Confidentiality Read Application Data - Race conditions may expose sensitive data through inconsistent state. |
| Access Control | Scope: Access Control Gain Privileges or Assume Identity - Corruption of security-critical state may allow privilege escalation. |
Example Code
Vulnerable Code
// Vulnerable: Single handler for multiple signals
#include <signal.h>
#include <syslog.h>
#include <stdlib.h>
char *logMessage;
void handler(int sigNum) {
// Vulnerable: Uses global variable
// Vulnerable: Calls non-reentrant syslog()
syslog(LOG_NOTICE, "%s\n", logMessage);
// Vulnerable: Can be interrupted mid-free
free(logMessage);
logMessage = NULL;
// Vulnerable: exit() is not async-signal-safe
exit(0);
}
int main(int argc, char* argv[]) {
logMessage = strdup("Program terminating");
// Vulnerable: Same handler for two signals
signal(SIGHUP, handler);
signal(SIGTERM, handler);
/* Attack scenario:
* 1. SIGHUP arrives, handler starts
* 2. syslog() internally calls malloc()
* 3. SIGTERM arrives before malloc() completes
* 4. Same handler invoked again
* 5. Second syslog() call corrupts heap metadata
* Or: double-free of logMessage
*/
while (1) {
// Main loop
}
return 0;
}
// Vulnerable: Handler with conditional logic based on signal
#include <signal.h>
int cleanup_in_progress = 0;
void *resource1 = NULL;
void *resource2 = NULL;
void multi_signal_handler(int sig) {
// Vulnerable: Race on cleanup_in_progress flag
if (cleanup_in_progress) {
return; // May not even reach this check
}
cleanup_in_progress = 1;
// Vulnerable: Between these frees, another signal can interrupt
free(resource1);
resource1 = NULL;
// If SIGTERM arrives here after SIGHUP started...
free(resource2); // First invocation also frees this
resource2 = NULL;
cleanup_in_progress = 0; // Race: may never be reached
}
int main() {
resource1 = malloc(100);
resource2 = malloc(200);
signal(SIGHUP, multi_signal_handler);
signal(SIGINT, multi_signal_handler);
signal(SIGTERM, multi_signal_handler);
while (1) { /* work */ }
}
// Vulnerable: Counter increment is not atomic
#include <signal.h>
int signal_count = 0; // Shared across handler invocations
void counter_handler(int sig) {
// Vulnerable: Read-modify-write is not atomic
signal_count++; // SIGUSR1 reads 5, SIGUSR2 also reads 5, both write 6
// If this handler does cleanup based on count...
if (signal_count >= 3) {
cleanup(); // May be called with wrong count
}
}
int main() {
signal(SIGUSR1, counter_handler);
signal(SIGUSR2, counter_handler);
while (1) { /* work */ }
}
// Vulnerable: Longjmp from signal handler
#include <signal.h>
#include <setjmp.h>
jmp_buf recovery_point;
int data_locked = 0;
void jump_handler(int sig) {
// Vulnerable: Longjmp leaves state inconsistent
longjmp(recovery_point, sig); // data_locked may stay 1
}
int main() {
signal(SIGHUP, jump_handler);
signal(SIGTERM, jump_handler);
if (setjmp(recovery_point) != 0) {
// Recovered from signal, but data_locked state is unknown
}
data_locked = 1;
// Critical section - if signal arrives here and longjmps...
process_critical_data();
data_locked = 0;
return 0;
}
Fixed Code
// Fixed: Separate handlers for each signal
#include <signal.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t hup_received = 0;
volatile sig_atomic_t term_received = 0;
void hup_handler(int sigNum) {
// Fixed: Only set flag for this specific signal
hup_received = 1;
}
void term_handler(int sigNum) {
// Fixed: Separate handler, separate flag
term_received = 1;
}
int main(int argc, char* argv[]) {
signal(SIGHUP, hup_handler);
signal(SIGTERM, term_handler);
while (!hup_received && !term_received) {
// Main loop
}
// Fixed: Cleanup in main context
if (hup_received) {
syslog(LOG_NOTICE, "Received SIGHUP, reloading config");
reload_config();
}
if (term_received) {
syslog(LOG_NOTICE, "Received SIGTERM, shutting down");
cleanup_and_exit();
}
return 0;
}
// Fixed: Block signals during handler execution
#include <signal.h>
void safe_handler(int sig) {
sigset_t mask, oldmask;
// Fixed: Block all related signals during handler
sigemptyset(&mask);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// Now safe from reentrancy via these signals
// Still should only do async-signal-safe operations
const char *msg = "Signal handled\n";
write(STDERR_FILENO, msg, strlen(msg));
// Restore signal mask
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
int main() {
struct sigaction sa;
sigset_t mask;
// Fixed: Use sigaction with sa_mask to auto-block
sigemptyset(&mask);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sa.sa_handler = safe_handler;
sa.sa_mask = mask; // Block these signals during handler
sa.sa_flags = 0;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
while (1) { /* work */ }
}
// Fixed: Use sig_atomic_t for shared counter
#include <signal.h>
#include <stdatomic.h>
// Fixed: Use atomic type
volatile sig_atomic_t signal_count = 0;
volatile sig_atomic_t shutdown_requested = 0;
void fixed_counter_handler(int sig) {
// Fixed: sig_atomic_t operations are atomic
signal_count++;
// For more complex logic, just set flag
shutdown_requested = 1;
}
int main() {
signal(SIGUSR1, fixed_counter_handler);
signal(SIGUSR2, fixed_counter_handler);
while (!shutdown_requested) {
// Main loop
}
// Handle in main context where complex operations are safe
printf("Received %d signals total\n", signal_count);
cleanup();
return 0;
}
// Fixed: Use signalfd for unified signal handling
#include <signal.h>
#include <sys/signalfd.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
sigset_t mask;
int sfd;
struct signalfd_siginfo fdsi;
// Block signals so they're delivered via signalfd
sigemptyset(&mask);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);
// Create signalfd
sfd = signalfd(-1, &mask, 0);
if (sfd == -1) {
perror("signalfd");
exit(1);
}
// Fixed: Handle signals in normal context via file descriptor
while (1) {
ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
if (s != sizeof(fdsi)) {
perror("read");
continue;
}
// Safe to do anything here - we're not in a signal handler
switch (fdsi.ssi_signo) {
case SIGHUP:
syslog(LOG_NOTICE, "Reloading configuration");
reload_config();
break;
case SIGINT:
case SIGTERM:
syslog(LOG_NOTICE, "Shutting down");
cleanup();
close(sfd);
exit(0);
}
}
return 0;
}
// Fixed: Self-pipe trick for portable signal handling
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
int signal_pipe[2];
void pipe_signal_handler(int sig) {
// Fixed: Only write signal number to pipe
unsigned char signum = (unsigned char)sig;
write(signal_pipe[1], &signum, 1); // write is async-signal-safe
}
int main() {
pipe(signal_pipe);
fcntl(signal_pipe[1], F_SETFL, O_NONBLOCK);
signal(SIGHUP, pipe_signal_handler);
signal(SIGTERM, pipe_signal_handler);
fd_set readfds;
while (1) {
FD_ZERO(&readfds);
FD_SET(signal_pipe[0], &readfds);
if (select(signal_pipe[0] + 1, &readfds, NULL, NULL, NULL) > 0) {
if (FD_ISSET(signal_pipe[0], &readfds)) {
unsigned char sig;
read(signal_pipe[0], &sig, 1);
// Fixed: Safe context for complex handling
switch (sig) {
case SIGHUP:
reload_config();
break;
case SIGTERM:
cleanup_and_exit();
break;
}
}
}
}
return 0;
}
Related CWEs
- CWE-364: Signal Handler Race Condition (parent)
- CWE-828: Signal Handler with Functionality that is not Asynchronous-Safe (related)
- CWE-479: Signal Handler Use of a Non-reentrant Function (related)
- CWE-662: Improper Synchronization (related)
References
- MITRE Corporation. "CWE-831: Signal Handler Function Associated with Multiple Signals." https://cwe.mitre.org/data/definitions/831.html
- CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
- CERT C Secure Coding Standard. "SIG31-C. Do not access shared objects in signal handlers."