Skip to content

CWE-479: Signal Handler Use of a Non-Reentrant Function

Overview

Signal handlers can interrupt program execution at any point, including during execution of non-reentrant functions. Calling non-reentrant functions (malloc, printf, pthread_mutex_lock) from signal handlers causes undefined behavior, deadlocks, data corruption, and crashes.

Risk

High: Non-reentrant functions in signal handlers cause deadlocks (acquiring already-held locks), heap corruption (malloc interrupted during allocation), data races, undefined behavior, and unpredictable crashes. Extremely difficult to debug.

Remediation Steps

Core principle: Signal handlers must use only async-signal-safe operations; avoid non-reentrant calls in handlers.

Locate Non-Reentrant Function Calls in Signal Handlers

When reviewing security scan results:

  • Examine data_paths: Identify signal handlers that call non-reentrant functions
  • Find signal handler definitions: Look for signal(), sigaction(), __attribute__((signal))
  • Check for unsafe calls: malloc/free, printf/fprintf, mutex locks, exit()
  • Identify shared state: Variables accessed by both handler and main code
  • Assess race potential: Can signal interrupt critical operations

Common non-reentrant functions to avoid:

  • Memory: malloc, free, realloc, calloc
  • I/O: printf, fprintf, sprintf, puts, putchar, fwrite
  • Locking: pthread_mutex_lock, sem_wait
  • Exit: exit() (use _exit() instead)

Use Only Async-Signal-Safe Functions (Primary Defense)

SAFE functions in signal handlers:

  • write() - use instead of printf
  • _exit() - use instead of exit()
  • Signal functions: signal, sigaction, sigprocmask
  • System calls: open, close, read (most basic syscalls)
  • Atomic operations on volatile sig_atomic_t variables

UNSAFE functions - never use in signal handlers:

  • malloc, free, realloc - heap allocation is not reentrant
  • printf, fprintf, sprintf, puts - stdio functions use internal locks
  • pthread_mutex_lock, pthread_mutex_unlock - can deadlock
  • exit() - use _exit() instead (exit() is not async-signal-safe)
  • Any function that may acquire locks or allocate memory

Safe example:

void handler(int sig) {
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);  // Safe
    _exit(1);  // Safe - use _exit, not exit
}

Why this works: Async-signal-safe functions are guaranteed not to use locks, allocate memory, or maintain internal state that could be corrupted by interruption.

Use Minimal Signal Handler Pattern (Set Flag Only)

#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // Only set flag - minimal, safe work
    got_signal = sig;
}

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

    while (1) {
        if (got_signal) {
            // Handle signal in main loop context, not in handler
            printf("Received signal %d\n", got_signal);
            // Can safely call any function here
            cleanup();
            break;
        }
        // Normal processing
        do_work();
    }
    return 0;
}

Why this works:

  • Signal handler only sets a volatile sig_atomic_t flag
  • All complex work happens in main loop
  • No non-reentrant functions in handler
  • Main loop can safely use printf, malloc, etc.

Use Self-Pipe Trick for Complex Signal Handling

For more complex scenarios where you need to pass data:

int signal_pipe[2];

void handler(int sig) {
    // Write signal number to pipe - async-signal-safe
    char byte = (char)sig;
    write(signal_pipe[1], &byte, 1);
}

int main() {
    pipe(signal_pipe);
    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    // Use select/poll to monitor pipe in main loop
    fd_set fds;
    while (1) {
        FD_ZERO(&fds);
        FD_SET(signal_pipe[0], &fds);

        if (select(signal_pipe[0] + 1, &fds, NULL, NULL, NULL) > 0) {
            char sig;
            read(signal_pipe[0], &sig, 1);
            // Now safely handle signal in main context
            printf("Received signal %d\n", sig);
            handle_signal(sig);
        }
    }
}

Benefits:

  • Signal handler only does safe write() to pipe
  • Main loop reads from pipe using select/poll
  • All complex handling in main context
  • Works with event loops

Use Modern Signal Handling APIs (signalfd on Linux)

Linux provides signalfd for cleaner signal handling:

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>

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

    // Block signals for signalfd
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);

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

    // Read signals like regular file descriptor
    while (1) {
        ssize_t s = read(sfd, &si, sizeof(si));
        if (s == sizeof(si)) {
            printf("Received signal %d\n", si.ssi_signo);
            // Safe to call any function here
            if (si.ssi_signo == SIGTERM) break;
        }
    }

    close(sfd);
    return 0;
}

Advantages:

  • No signal handler needed
  • Signals delivered as file descriptor events
  • Integrates with select/poll/epoll
  • All handling in normal context

Test Signal Handler Safety

Testing strategies:

  • Send signals during execution: kill -SIGNAL $PID
  • Send rapid signals to test race conditions
  • Use ThreadSanitizer to detect data races: gcc -fsanitize=thread
  • Test with different signal types: SIGINT, SIGTERM, SIGUSR1
  • Verify no deadlocks occur

Stress testing:

# Send rapid signals to trigger races
while true; do kill -USR1 $PID; sleep 0.001; done &
./your_program

Static analysis:

  • Review all signal handlers for non-async-signal-safe calls
  • Check for malloc/free in handlers
  • Look for printf/stdio in handlers
  • Verify no mutex operations in handlers

Common Vulnerable Patterns

void handler(int sig) {
    // UNSAFE - malloc not reentrant
    char *msg = malloc(100);

    // UNSAFE - printf not async-signal-safe
    printf("Signal %d\n", sig);

    // UNSAFE - can deadlock
    pthread_mutex_lock(&lock);

    // UNSAFE - exit() not async-signal-safe
    exit(1);  // Use _exit(1)
}

Why This Fails

// Main thread
pthread_mutex_lock(&lock);  // Acquired
// ... SIGNAL INTERRUPTS HERE ...

// Signal handler
pthread_mutex_lock(&lock);  // DEADLOCK! Already held

Security Checklist

  • No malloc/free/realloc in signal handlers
  • No printf/fprintf/sprintf in signal handlers
  • No pthread_mutex_lock in signal handlers
  • Using _exit() not exit() if exiting from handler
  • Shared variables are volatile sig_atomic_t
  • Complex work moved to main loop or using self-pipe/signalfd

Additional Resources