Signal Handler Use of a Non-reentrant Function

Description

Signal Handler Use of a Non-reentrant Function is a vulnerability where a signal handler calls a function that is not designed to be safely interrupted and re-invoked. Non-reentrant functions cannot safely be interrupted during execution and called again before their initial invocation completes. When such functions are called from signal handlers, they can enter undefined states causing memory corruption. Functions like syslog(), malloc(), free(), and many standard library functions are non-reentrant because they rely on global data structures or static buffers that become corrupted under concurrent access.

Risk

Signal handlers that call non-reentrant functions create serious security vulnerabilities. Memory corruption can occur when the signal handler interrupts the same function mid-execution, corrupting global data structures. For example, if malloc() is interrupted by a signal handler that also calls malloc(), the heap metadata becomes corrupted, potentially enabling write-what-where conditions for arbitrary code execution. These race conditions are difficult to reproduce during testing but can be triggered reliably by attackers who control signal timing. The vulnerability is particularly dangerous because it can turn signal-handling code into an exploitable primitive.

Solution

Design signal handlers to perform minimal operations, typically just setting a flag that the main program checks. Avoid calling any non-reentrant functions from signal handlers. Only use async-signal-safe functions as defined by POSIX (such as write(), _exit(), signal functions). If complex operations are needed upon signal receipt, have the handler set a flag and perform the actual work in the main program loop. Consider using alternative mechanisms like signalfd() on Linux or sigwait() to handle signals synchronously. Document which functions in your codebase are signal-safe.

Common Consequences

ImpactDetails
IntegrityScope: Integrity, Availability

Modify Memory - Signal race conditions frequently cause data corruption when non-reentrant functions have their internal state corrupted by concurrent invocation.
Code ExecutionScope: Confidentiality, Integrity, Availability

Execute Unauthorized Code - Memory corruption from heap metadata corruption can enable arbitrary code execution through write-what-where conditions.

Example Code

Vulnerable Code

// Vulnerable: Signal handler calls non-reentrant functions
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>

char *global_buffer = NULL;

// Vulnerable: Handler calls malloc, syslog - both non-reentrant
void vulnerable_signal_handler(int sig) {
    // Vulnerable: syslog() allocates scratch memory internally
    // If signal arrives while syslog() is executing, corruption occurs
    syslog(LOG_ERR, "Received signal %d", sig);

    // Vulnerable: malloc() uses global heap metadata
    // Interrupting malloc() and calling it again corrupts the heap
    char *temp = malloc(100);
    if (temp) {
        sprintf(temp, "Signal %d received", sig);
        // Vulnerable: free() is also non-reentrant
        free(global_buffer);
        global_buffer = temp;
    }

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

void setup_vulnerable_handler() {
    signal(SIGINT, vulnerable_signal_handler);
    signal(SIGTERM, vulnerable_signal_handler);
}

// Vulnerable: Complex signal handler with file operations
void vulnerable_logging_handler(int sig) {
    FILE *log = fopen("/var/log/app.log", "a");
    if (log) {
        // Vulnerable: fprintf is not async-signal-safe
        fprintf(log, "Signal %d at time %ld\n", sig, time(NULL));
        fclose(log);
    }

    // Vulnerable: Calling cleanup functions that use malloc/free
    cleanup_resources();
}

// Vulnerable: Handler that modifies shared state unsafely
static int request_count = 0;
static char *last_request = NULL;

void vulnerable_stat_handler(int sig) {
    // Vulnerable: Non-atomic operations on shared state
    // Can corrupt data if signal arrives mid-update
    char buffer[256];
    sprintf(buffer, "Requests: %d, Last: %s", request_count, last_request);

    // Vulnerable: strlen, memcpy not guaranteed signal-safe
    size_t len = strlen(buffer);
    write(STDOUT_FILENO, buffer, len);  // write IS signal-safe, but setup isn't
}
# Vulnerable: Python signal handler with non-reentrant operations
import signal
import logging
import sys

# Vulnerable: Global logger configuration
logger = logging.getLogger(__name__)

def vulnerable_signal_handler(signum, frame):
    # Vulnerable: Logging is not signal-safe in Python
    # Can cause deadlocks if signal arrives during logging
    logger.error(f"Received signal {signum}")

    # Vulnerable: print() is not reentrant
    print(f"Signal {signum} caught, cleaning up...")

    # Vulnerable: Complex cleanup operations
    cleanup_connections()
    flush_caches()

    sys.exit(1)

def setup_handlers():
    signal.signal(signal.SIGINT, vulnerable_signal_handler)
    signal.signal(signal.SIGTERM, vulnerable_signal_handler)

Fixed Code

// Fixed: Signal handler only sets flags, work done elsewhere
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>
#include <unistd.h>

// Fixed: Use volatile sig_atomic_t for signal flags
static volatile sig_atomic_t got_sigint = 0;
static volatile sig_atomic_t got_sigterm = 0;

// Fixed: Minimal signal handler that only sets a flag
void safe_signal_handler(int sig) {
    // Fixed: Only set flags - this is async-signal-safe
    if (sig == SIGINT) {
        got_sigint = 1;
    } else if (sig == SIGTERM) {
        got_sigterm = 1;
    }
    // Fixed: write() is async-signal-safe if we must output something
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

void setup_safe_handler() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = safe_signal_handler;
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigemptyset(&sa.sa_mask);

    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
}

// Fixed: Main loop checks flags and does actual work
void main_loop() {
    while (1) {
        // Fixed: Check signal flags in main context
        if (got_sigint) {
            got_sigint = 0;
            // Safe to call non-reentrant functions here
            syslog(LOG_INFO, "Handling SIGINT");
            handle_interrupt();
        }

        if (got_sigterm) {
            syslog(LOG_INFO, "Handling SIGTERM - shutting down");
            cleanup_and_exit();
            break;
        }

        // Do normal work
        process_requests();
    }
}

// Fixed: Using self-pipe trick for signal handling
static int signal_pipe[2];

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

void setup_signal_pipe() {
    pipe(signal_pipe);

    // Make write end non-blocking
    int flags = fcntl(signal_pipe[1], F_GETFL);
    fcntl(signal_pipe[1], F_SETFL, flags | O_NONBLOCK);

    struct sigaction sa;
    sa.sa_handler = safe_handler_with_pipe;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
}

void event_loop_with_pipe() {
    fd_set readfds;

    while (1) {
        FD_ZERO(&readfds);
        FD_SET(signal_pipe[0], &readfds);
        // Add other file descriptors...

        if (select(signal_pipe[0] + 1, &readfds, NULL, NULL, NULL) > 0) {
            if (FD_ISSET(signal_pipe[0], &readfds)) {
                char sig;
                read(signal_pipe[0], &sig, 1);
                // Fixed: Safe to call any function here
                syslog(LOG_INFO, "Signal %d received", sig);
                handle_signal((int)sig);
            }
        }
    }
}
# Fixed: Safe signal handling in Python
import signal
import sys
import logging
import threading

logger = logging.getLogger(__name__)

# Fixed: Use flag-based approach
shutdown_requested = threading.Event()

def safe_signal_handler(signum, frame):
    # Fixed: Only set flag, don't do complex operations
    shutdown_requested.set()

def setup_handlers():
    signal.signal(signal.SIGINT, safe_signal_handler)
    signal.signal(signal.SIGTERM, safe_signal_handler)

def main_loop():
    while not shutdown_requested.is_set():
        # Do normal work
        process_request()

        # Check for shutdown periodically
        if shutdown_requested.wait(timeout=0.1):
            break

    # Fixed: Safe to do cleanup in main context
    logger.info("Shutdown requested, cleaning up...")
    cleanup_connections()
    flush_caches()
    sys.exit(0)

# Fixed: Alternative using signal.set_wakeup_fd (Python 3.5+)
import os
import selectors

def setup_with_wakeup_fd():
    read_fd, write_fd = os.pipe()
    os.set_blocking(write_fd, False)

    # Signal writes to this fd
    signal.set_wakeup_fd(write_fd)
    signal.signal(signal.SIGINT, lambda s, f: None)

    return read_fd

def event_loop_with_wakeup(signal_fd):
    sel = selectors.DefaultSelector()
    sel.register(signal_fd, selectors.EVENT_READ)

    while True:
        events = sel.select(timeout=1.0)
        for key, mask in events:
            if key.fd == signal_fd:
                # Drain the signal bytes
                os.read(signal_fd, 1024)
                # Fixed: Safe to do complex work here
                logger.info("Signal received, handling...")
                handle_shutdown()
                return

CVE Examples

  • CVE-2005-0893: Signal handler in wu-ftpd calls function that uses malloc(), allowing remote attackers to cause denial of service or execute code via signals sent during memory allocation.
  • CVE-2004-2259: SIGCHLD signal handler in vsftpd FTP server can be triggered to cause a crash when malloc() or free() is being executed, leading to denial of service under high load.

References

  1. MITRE Corporation. "CWE-479: Signal Handler Use of a Non-reentrant Function." https://cwe.mitre.org/data/definitions/479.html
  2. CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
  3. POSIX.1-2017. "Signal Concepts - Async-Signal-Safe Functions."