Signal Handler with Functionality that is not Asynchronous-Safe

Description

Signal Handler with Functionality that is not Asynchronous-Safe is a concurrency vulnerability where a signal handler contains code sequences that are not safe to execute in an asynchronous context. Signal handlers can be invoked at any point during program execution, interrupting any currently executing code. If the handler uses non-reentrant functions (like malloc(), printf(), or syslog()), accesses global or static variables non-atomically, or can be interrupted by other signals, the program state can become corrupted. This corruption can lead to crashes, undefined behavior, or exploitable security vulnerabilities.

Risk

This vulnerability can result in memory corruption, denial of service, or code execution. When a signal handler calls non-reentrant functions like malloc(), the handler might interrupt the same function already in progress in the main code, corrupting heap metadata and leading to crashes or exploitation. Global variables accessed in handlers may be left in inconsistent states when handlers are interrupted. Attackers who can send signals to a process may time their attacks to corrupt critical state, leading to privilege escalation or security bypass. Double-free vulnerabilities are common when signal handlers can be invoked multiple times while freeing the same memory.

Solution

Only use async-signal-safe functions in signal handlers. POSIX defines a specific list of functions that are safe to call from signal handlers—restrict handlers to this list. Do not allocate or free memory in handlers. Use volatile sig_atomic_t for any variables shared between handlers and main code. Block signals during critical sections that access shared state. Design handlers to set a flag that the main loop checks, rather than performing complex operations directly. If complex processing is needed, have the handler write to a pipe that the main program reads. Consider using signalfd() or similar mechanisms that integrate signals with event loops.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - State corruption from non-reentrant function calls or race conditions causes crashes or hangs.
Integrity, Confidentiality, AvailabilityScope: Integrity, Confidentiality, Availability

Execute Unauthorized Code or Commands - When handlers corrupt security-critical state, attackers may bypass protections or achieve code execution.

Example Code

Vulnerable Code

// Vulnerable: Handler calls non-reentrant functions
#include <signal.h>
#include <syslog.h>
#include <stdlib.h>

char *logMessage;

void handler(int sigNum) {
    // Vulnerable: syslog() is not async-signal-safe
    // It internally calls malloc() which is not reentrant
    syslog(LOG_NOTICE, "%s\n", logMessage);

    // Vulnerable: free() is not async-signal-safe
    free(logMessage);

    exit(0);
}

int main() {
    logMessage = strdup("Shutting down");
    signal(SIGHUP, handler);
    signal(SIGTERM, handler);

    // Main processing loop
    while (1) {
        // If SIGHUP arrives during malloc() in main code,
        // syslog()'s malloc() corrupts heap
    }
}
// Vulnerable: Double-free via signal reentrancy
#include <signal.h>
#include <stdlib.h>

char *global1;
char *global2;

void sh(int dummy) {
    // Vulnerable: Can be called twice if two signals arrive
    syslog(LOG_NOTICE, "%s\n", global1);
    free(global2);
    free(global1);  // If handler is reentered, double-free!
    exit(0);
}

int main() {
    global1 = strdup("message");
    global2 = strdup("data");

    signal(SIGHUP, sh);
    signal(SIGTERM, sh);

    // SIGHUP arrives, starts freeing global1
    // SIGTERM arrives before exit(), calls sh() again
    // global1 freed twice!

    while (1) { /* work */ }
}
// Vulnerable: Accessing global variable non-atomically
#include <signal.h>

// Not volatile, not atomic
int transaction_in_progress = 0;
int transaction_count = 0;

void handler(int sig) {
    // Vulnerable: Non-atomic read-modify-write
    if (transaction_in_progress) {
        transaction_count++;  // May corrupt if interrupted mid-update
    }

    // Vulnerable: printf is not async-signal-safe
    printf("Transactions: %d\n", transaction_count);
}

int main() {
    signal(SIGUSR1, handler);
    while (1) {
        transaction_in_progress = 1;
        // ... do transaction ...
        transaction_in_progress = 0;
    }
}
// Vulnerable: Handler modifies shared data structures
#include <signal.h>

struct node {
    int data;
    struct node *next;
};

struct node *list_head = NULL;

void cleanup_handler(int sig) {
    // Vulnerable: Modifying linked list during signal
    // Main code might be traversing the same list
    struct node *curr = list_head;
    while (curr) {
        struct node *next = curr->next;
        free(curr);  // Also: free() not async-signal-safe
        curr = next;
    }
    list_head = NULL;
}
// Vulnerable: Using stdio in signal handler
#include <signal.h>
#include <stdio.h>

FILE *logfile;

void handler(int sig) {
    // Vulnerable: fprintf/fflush not async-signal-safe
    // If main code is also using stdio, buffer corruption
    fprintf(logfile, "Signal %d received\n", sig);
    fflush(logfile);
}

Fixed Code

// Fixed: Use only async-signal-safe functions
#include <signal.h>
#include <unistd.h>
#include <string.h>

volatile sig_atomic_t shutdown_requested = 0;

void handler(int sigNum) {
    // Fixed: Only set a flag
    shutdown_requested = 1;

    // If you must output, use write() which is async-signal-safe
    const char *msg = "Shutdown signal received\n";
    write(STDERR_FILENO, msg, strlen(msg));
}

int main() {
    char *logMessage = strdup("Shutting down");

    signal(SIGHUP, handler);
    signal(SIGTERM, handler);

    while (!shutdown_requested) {
        // Main processing
    }

    // Cleanup in main context where it's safe
    syslog(LOG_NOTICE, "%s\n", logMessage);
    free(logMessage);

    return 0;
}
// Fixed: Block signals during critical sections
#include <signal.h>
#include <stdlib.h>

char *global1;
char *global2;
volatile sig_atomic_t should_exit = 0;

void sh(int dummy) {
    // Fixed: Just set flag
    should_exit = 1;
}

int main() {
    sigset_t mask, oldmask;

    global1 = strdup("message");
    global2 = strdup("data");

    signal(SIGHUP, sh);
    signal(SIGTERM, sh);

    while (!should_exit) {
        // Work
    }

    // Fixed: Block signals during cleanup
    sigemptyset(&mask);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, &oldmask);

    // Safe cleanup - signals blocked
    syslog(LOG_NOTICE, "%s\n", global1);
    free(global2);
    free(global1);

    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return 0;
}
// Fixed: Use volatile sig_atomic_t for shared variables
#include <signal.h>
#include <stdatomic.h>

// Fixed: Proper types for signal handler access
volatile sig_atomic_t transaction_in_progress = 0;
volatile sig_atomic_t signal_received = 0;

void handler(int sig) {
    // Fixed: Only access volatile sig_atomic_t
    signal_received = 1;

    // Don't do complex operations here
}

int main() {
    signal(SIGUSR1, handler);

    while (1) {
        transaction_in_progress = 1;
        // ... do transaction ...
        transaction_in_progress = 0;

        // Check signal flag in main loop
        if (signal_received) {
            printf("Signal received, transaction status: %d\n",
                   transaction_in_progress);
            signal_received = 0;
        }
    }
}
// Fixed: Use self-pipe trick for complex handler logic
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int signal_pipe[2];

void handler(int sig) {
    // Fixed: write() is async-signal-safe
    char c = (char)sig;
    write(signal_pipe[1], &c, 1);
}

int main() {
    pipe(signal_pipe);

    // Make write end non-blocking
    fcntl(signal_pipe[1], F_SETFL, O_NONBLOCK);

    signal(SIGHUP, handler);
    signal(SIGTERM, handler);

    while (1) {
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(signal_pipe[0], &fds);
        FD_SET(STDIN_FILENO, &fds);

        // select() on both regular I/O and signal pipe
        if (select(signal_pipe[0] + 1, &fds, NULL, NULL, NULL) > 0) {
            if (FD_ISSET(signal_pipe[0], &fds)) {
                char sig;
                read(signal_pipe[0], &sig, 1);
                // Now safe to do complex processing in main context
                printf("Received signal %d\n", (int)sig);
                // Can safely call syslog, free, etc. here
            }
        }
    }
}
// Fixed: Use signalfd() (Linux-specific)
#include <signal.h>
#include <sys/signalfd.h>
#include <unistd.h>

int main() {
    sigset_t mask;
    int sfd;
    struct signalfd_siginfo fdsi;

    sigemptyset(&mask);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);

    // Block signals so they go to signalfd
    sigprocmask(SIG_BLOCK, &mask, NULL);

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

    while (1) {
        ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
        if (s == sizeof(fdsi)) {
            // Safe to do anything here - we're in normal context
            printf("Received signal %d\n", fdsi.ssi_signo);
            syslog(LOG_NOTICE, "Signal %d handled", fdsi.ssi_signo);

            if (fdsi.ssi_signo == SIGTERM) {
                break;
            }
        }
    }

    close(sfd);
    return 0;
}

  • CWE-364: Signal Handler Race Condition (parent)
  • CWE-479: Signal Handler Use of a Non-reentrant Function (child)
  • CWE-831: Signal Handler Function Associated with Multiple Signals (related)
  • CWE-662: Improper Synchronization (related)

References

  1. MITRE Corporation. "CWE-828: Signal Handler with Functionality that is not Asynchronous-Safe." https://cwe.mitre.org/data/definitions/828.html
  2. CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
  3. POSIX. "Signal Concepts - Async-Signal-Safe Functions."