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 ofprintf_exit()- use instead ofexit()- 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 reentrantprintf,fprintf,sprintf,puts- stdio functions use internal lockspthread_mutex_lock,pthread_mutex_unlock- can deadlockexit()- 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_tflag - 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/freein handlers - Look for
printf/stdioin 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/reallocin signal handlers - No
printf/fprintf/sprintfin signal handlers - No
pthread_mutex_lockin signal handlers - Using
_exit()notexit()if exiting from handler - Shared variables are volatile
sig_atomic_t - Complex work moved to main loop or using self-pipe/signalfd