Signal Handler Race Condition

Description

Signal Handler Race Condition is a vulnerability that occurs when signal handlers introduce race conditions through asynchronous actions. Signals can interrupt normal program execution at virtually any point, causing signal handlers to run in the context of the interrupted code. If signal handlers use non-reentrant functions, modify global variables, or perform state-sensitive operations, they may violate assumptions made by the interrupted code or other signal handlers. The core issue is that signals can arrive during critical sections where data structures are in inconsistent states, leading to corruption, crashes, or exploitable conditions when the handler accesses these structures.

Risk

Signal handler race conditions can lead to severe security vulnerabilities including arbitrary code execution, privilege escalation, and denial of service. When handlers call non-reentrant functions like malloc/free while the main program is also in malloc/free, double-free or use-after-free conditions can occur. Handlers that modify global state can corrupt data structures. Attackers can remotely trigger signals (like SIGURG for urgent TCP data) to exploit these conditions. The unpredictable timing makes these vulnerabilities difficult to reproduce but attackers can increase their chances through repeated attempts or by controlling when signals arrive.

Solution

Keep signal handlers minimal - ideally only setting a flag that the main program checks. Only call async-signal-safe functions from handlers (documented in signal-safety(7) on Linux). Block signals during critical sections using sigprocmask. Avoid accessing global variables in handlers unless they are volatile sig_atomic_t. Use sigaction with SA_RESTART to handle interrupted system calls. Consider using signalfd() on Linux to handle signals synchronously. Never call malloc, free, printf, or other non-reentrant functions from signal handlers.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Data corruption and possible arbitrary code execution by modifying global variables at unexpected times.
AvailabilityScope: Availability

System crashes from double-free or corrupted data structures when handlers interrupt critical sections.
Access ControlScope: Access Control

Privilege escalation when handlers interrupt code running with elevated privileges.

Example Code

Vulnerable Code

// Vulnerable: Non-reentrant function in signal handler
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

char *global_buffer = NULL;

void vulnerable_handler(int sig) {
    // Vulnerable: free() is not async-signal-safe
    // If signal arrives during malloc/free, corruption occurs
    free(global_buffer);
    global_buffer = malloc(100);  // Also unsafe

    // Vulnerable: printf is not async-signal-safe
    printf("Signal received\n");
}

int main() {
    signal(SIGINT, vulnerable_handler);

    while (1) {
        // If signal arrives here, during malloc, double-free can occur
        global_buffer = malloc(1000);
        // ... use buffer ...
        free(global_buffer);
    }
}
// Vulnerable: Global state modification in handler
volatile int counter = 0;
volatile int processing = 0;

void vulnerable_handler(int sig) {
    // Vulnerable: Non-atomic read-modify-write
    if (processing) {
        counter++;  // May race with main program
    }
}

void process_data() {
    processing = 1;
    // Vulnerable: Signal can arrive between these statements
    counter = 0;
    // ... process ...
    int result = counter;  // May have unexpected value
    processing = 0;
}

Fixed Code

// Fixed: Minimal signal handler with flag
#include <signal.h>
#include <stdlib.h>
#include <string.h>

volatile sig_atomic_t signal_received = 0;

void secure_handler(int sig) {
    // Fixed: Only set flag - async-signal-safe
    signal_received = 1;
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = secure_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);

    char *buffer = NULL;
    while (1) {
        // Fixed: Check flag in main loop
        if (signal_received) {
            signal_received = 0;
            // Handle signal safely in main context
            if (buffer) free(buffer);
            buffer = malloc(100);
        }

        // Normal processing with signals blocked during critical section
        sigset_t block_set, old_set;
        sigemptyset(&block_set);
        sigaddset(&block_set, SIGINT);

        sigprocmask(SIG_BLOCK, &block_set, &old_set);
        // Critical section - signals blocked
        buffer = realloc(buffer, 1000);
        sigprocmask(SIG_SETMASK, &old_set, NULL);
    }
}
// Fixed: Using signalfd for synchronous signal handling (Linux)
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);

    // Block signals so they're delivered via signalfd
    sigprocmask(SIG_BLOCK, &mask, NULL);

    // Create signalfd
    int sfd = signalfd(-1, &mask, 0);

    while (1) {
        struct signalfd_siginfo fdsi;
        ssize_t s = read(sfd, &fdsi, sizeof(fdsi));

        if (s == sizeof(fdsi)) {
            // Fixed: Handle signal synchronously - all functions safe
            printf("Received signal %d\n", fdsi.ssi_signo);
            // Can safely call any function here
        }
    }
}

CVE Examples

  • CVE-1999-0035 — Signal handler allowed access to files with elevated privileges.
  • CVE-2001-0905 — Signal interruption led to root execution.
  • CVE-2004-0794 — Remote SIGURG signal exploited handler logic.
  • CVE-2004-2259 — SIGCHLD during malloc caused crashes.

References

  1. MITRE Corporation. "CWE-364: Signal Handler Race Condition." https://cwe.mitre.org/data/definitions/364.html
  2. Linux man-pages. "signal-safety(7)." https://man7.org/linux/man-pages/man7/signal-safety.7.html