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

ImpactDetails
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - State corruption from reentrant handler invocations causes crashes or hangs.
Integrity, Confidentiality, AvailabilityScope: Integrity, Confidentiality, Availability

Execute Unauthorized Code or Commands - Corruption of heap metadata or function pointers may enable code execution.
ConfidentialityScope: Confidentiality

Read Application Data - Race conditions may expose sensitive data through inconsistent state.
Access ControlScope: 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;
}

  • 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

  1. MITRE Corporation. "CWE-831: Signal Handler Function Associated with Multiple Signals." https://cwe.mitre.org/data/definitions/831.html
  2. CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
  3. CERT C Secure Coding Standard. "SIG31-C. Do not access shared objects in signal handlers."