Chapter 21.1.2 — Reentrant & Async-Signal-Safe Functions

 

Chapter 21.1.2 — Reentrant & Async-Signal-Safe Functions
Linux Systems Programming · EmbeddedPathashala
21.1.2
Section
3
Code Examples
7
Interview Qs

Key Terms
Reentrant Function Async-Signal-Safe Non-reentrant Static Storage malloc() crypt() errno savedErrno write()

What Does “Reentrant” Mean?

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.

✅ Reentrant Function
Uses only local variables
Does not update global/static data
Does not use non-reentrant library calls
❌ Non-Reentrant Function
Uses static/global data structures
Returns pointer to static buffer
Calls malloc()/free() (uses shared heap list)

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

Async-Signal-Safe Functions

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.

The Golden Rule: Call ONLY async-signal-safe functions from within a signal handler. Calling any other function is technically undefined behaviour and risks data corruption or deadlocks.

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()
Note on errno: Even async-signal-safe functions can modify 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.

Code Example 1 — The crypt() Non-Reentrancy Problem

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;
}
Compile: gcc -Wall -o crypt_demo crypt_demo.c -lcrypt
Run: ./crypt_demo abc def then press Ctrl+C rapidly. You will see mismatches demonstrating non-reentrancy.

Code Example 2 — Saving and Restoring errno in a Signal Handler

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;
}
Always save/restore errno in any signal handler that calls async-signal-safe functions. Forgetting this is a subtle bug that only appears under race conditions.

Code Example 3 — Using write() Instead of printf() in a Handler

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;
}
Pattern: Prepare all output strings before you need them in the handler, or use write() with fixed string literals. Never use sprintf() + printf() inside a handler.

Interview Questions — Reentrancy & Async-Signal-Safety
Q1. What makes a function non-reentrant? Give three examples from the C standard library.
A function is non-reentrant if it: (1) uses static or global data structures (e.g., 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.
Q2. What is the difference between a reentrant function and an async-signal-safe function?
All reentrant functions are async-signal-safe. But some functions may be async-signal-safe without being fully reentrant — they may be non-interruptible by the implementation’s guarantees. In practice, POSIX defines a specific list of functions guaranteed to be async-signal-safe; outside that list, assume unsafe.
Q3. Why is malloc() unsafe inside a signal handler?
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.
Q4. Why must you save and restore errno in a signal handler?
Async-signal-safe functions can still modify 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.
Q5. Can you call exit() from a signal handler? What should you call instead?
No. 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.
Q6. Is strlen() safe to call from a signal handler?
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.
Q7. What output function is safe to use in a signal handler, and why?
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.

Next: Global Variables & sig_atomic_t →

Chapter 21.1.3 — sig_atomic_t ← Previous

Leave a Reply

Your email address will not be published. Required fields are marked *