A function is reentrant if it can be safely called simultaneously by multiple threads of execution — and still produce correct results each time.
In the context of signal handlers, even a single-threaded program has two execution paths that can interleave: the main program and the signal handler. The signal handler can interrupt the main program at literally any instruction — even in the middle of a library function call.
Examples of non-reentrant functions in the C library:
| Function | Why Non-Reentrant |
|---|---|
printf(), scanf() |
Uses internal buffered stdio data structures |
malloc(), free() |
Maintains a linked list of free heap blocks |
crypt() |
Returns pointer to a static buffer |
getpwnam() |
Returns pointer to static structure |
gethostbyname() |
Returns pointer to static structure |
strtok() |
Keeps state in a static pointer between calls |
An async-signal-safe function is one that is safe to call from within a signal handler. A function is async-signal-safe if it is either reentrant OR guaranteed not to be interrupted by a signal handler in a way that causes problems.
Important async-signal-safe functions you can safely call from a handler:
_exit() write() read() open() close() kill() fork() sigaction() sigprocmask() sem_post() alarm() waitpid()Functions you must NOT call from a handler:
printf() malloc() free() exit() strtok() getpwnam() crypt()errno. If your handler calls any safe function that might set errno, you must save and restore errno at the start and end of the handler.This reproduces the classic non-reentrancy bug from the textbook. Both main() and the signal handler call crypt(), which uses a single static buffer. When the handler interrupts main between the crypt() call and the string comparison, it overwrites the static buffer, causing a mismatch.
#define _XOPEN_SOURCE 600
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static char *str2;
static volatile sig_atomic_t handled = 0;
static void handler(int sig)
{
/*
* crypt() is NOT async-signal-safe — it uses static storage.
* Calling it here may overwrite the buffer that main() is using.
*/
crypt(str2, "xx"); /* UNSAFE */
handled++;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage: %s str1 str2\n", argv[0]);
return 1;
}
str2 = argv[2];
/* Encrypt str1 and save a copy */
char *cr1 = strdup(crypt(argv[1], "xx"));
if (!cr1) { perror("strdup"); return 1; }
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
sigaction(SIGINT, &sa, NULL);
int mismatch = 0;
for (int callNum = 1; ; callNum++) {
/*
* If SIGINT fires between crypt() and strcmp(), the handler's
* crypt(str2,...) overwrites the static buffer, so strcmp() fails.
*/
if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
mismatch++;
printf("Mismatch on call %d (mismatch=%d handled=%d)\n",
callNum, mismatch, handled);
}
/* Press Ctrl+C repeatedly to trigger mismatches */
if (mismatch >= 5) break;
}
free(cr1);
return 0;
}
gcc -Wall -o crypt_demo crypt_demo.c -lcryptRun:
./crypt_demo abc def then press Ctrl+C rapidly. You will see mismatches demonstrating non-reentrancy.Even async-signal-safe functions can modify errno. The correct pattern is to always save and restore errno in your signal handler.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
static volatile sig_atomic_t child_exited = 0;
static void sigchld_handler(int sig)
{
/*
* Save errno FIRST — the functions we call below (waitpid, write)
* may set errno, which would corrupt errno in the interrupted main code.
*/
int saved_errno = errno;
/* write() is async-signal-safe */
const char msg[] = "[Handler] SIGCHLD received\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
child_exited = 1;
/* Restore errno BEFORE returning */
errno = saved_errno;
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
printf("Forking a child...\n");
pid_t pid = fork();
if (pid == 0) {
/* Child: do some work then exit */
sleep(2);
printf("[Child] Exiting now.\n");
_exit(0);
}
/* Parent: wait for the flag */
while (!child_exited) {
pause();
}
printf("[Parent] Detected child exit. errno is intact: %d\n", errno);
return 0;
}
Since printf() is unsafe, use write() with pre-formatted strings if you must output from a handler.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
/* Helper: write a string literal to STDOUT from a signal handler safely */
static void safe_write(const char *msg)
{
write(STDOUT_FILENO, msg, strlen(msg));
/* Note: strlen() itself is reentrant (uses only local state) */
}
/* Helper: write a small unsigned integer safely (no printf!) */
static void safe_write_uint(unsigned int n)
{
char buf[20];
int i = sizeof(buf) - 1;
buf[i] = '\0';
if (n == 0) { buf[--i] = '0'; }
else {
while (n > 0) { buf[--i] = '0' + (n % 10); n /= 10; }
}
write(STDOUT_FILENO, buf + i, sizeof(buf) - 1 - i);
}
static volatile sig_atomic_t signal_count = 0;
static void sigusr1_handler(int sig)
{
int saved_errno = errno;
signal_count++;
safe_write("[Handler] SIGUSR1 received. Count = ");
safe_write_uint((unsigned int)signal_count);
safe_write("\n");
errno = saved_errno;
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = sigusr1_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGUSR1, &sa, NULL);
printf("PID = %d. Send: kill -USR1 %d\n",
(int)getpid(), (int)getpid());
while (signal_count < 5) pause();
printf("[Main] Received %d signals total.\n", (int)signal_count);
return 0;
}
write() with fixed string literals. Never use sprintf() + printf() inside a handler.crypt(), getpwnam() — return static buffers), (2) uses shared heap management (e.g., malloc()/free()), or (3) uses internal state between calls (e.g., strtok()). If interrupted mid-execution and called again, the shared state becomes corrupted.malloc() and free() maintain a linked list of freed memory blocks. If a signal interrupts the main program while it is inside malloc() (in the middle of updating this linked list) and the handler also calls malloc(), the linked list gets corrupted, causing heap corruption, crashes, or security vulnerabilities.errno. If your handler calls one (e.g., waitpid()), it may overwrite an errno value that the interrupted main program code was about to read. This corrupts the main program’s error handling. Solution: save errno at handler entry, restore it before returning.exit() is NOT async-signal-safe because it flushes stdio buffers (calls into the non-reentrant stdio library). You must call _exit() instead, which immediately terminates the process without flushing stdio buffers.strlen() only reads memory sequentially using a local pointer variable — it doesn’t use any global or static state. So it is reentrant in practice. However, it is NOT in the official POSIX async-signal-safe list, so strictly speaking you should avoid it. In practice, it is safe on all known implementations.write() is async-signal-safe because it is a direct system call that does not use any userspace buffering or shared data structures. Use write(STDOUT_FILENO, msg, len) instead of printf() in signal handlers.