Dangers of malloc and printf in C Signal Handlers

In C, asynchronous signal handlers interrupt the normal flow of program execution at arbitrary points, meaning they can run while any function in the main execution context is partially complete. Invoking non-reentrant functions like malloc() or printf() inside an asynchronous signal handler causes severe risks, notably deadlocks, memory corruption, and undefined behavior. Because standard heap allocation and formatted I/O functions rely on shared global states and internal locks, they violate POSIX async-signal-safety requirements and must be avoided inside signal handling routines.

Non-Reentrancy and Async-Signal-Safety

A function is reentrant if it can be interrupted in the middle of its execution and safely called again (reentered) before the previous invocation completes. A subset of reentrant functions is classified by POSIX as "async-signal-safe." These functions are guaranteed to be safe when invoked from an asynchronous signal handler.

Functions like malloc() and printf() are not async-signal-safe. When a hardware or software signal is delivered, the operating system pauses the main thread at its current CPU instruction and transfers control to the registered signal handler. If the main thread was interrupted while executing an internal section of malloc() or printf(), the state of those functions is half-modified.

Why Allocating Heap Memory (malloc) is Dangerous

Dynamic memory allocation functions—including malloc(), calloc(), realloc(), and free()—manage shared metadata structures that track free and used blocks of memory across the heap.

  1. Deadlocks: To prevent race conditions in multi-threaded programs, modern C standard libraries wrap heap operations with internal mutexes. If the main program calls malloc() and acquires the allocator lock, and a signal arrives before that lock is released, the signal handler runs on the same thread. If the handler calls malloc(), it attempts to acquire the exact same lock. Because the lock holder (the main program thread) is suspended waiting for the signal handler to return, the signal handler waits indefinitely for the lock to be released, resulting in an immediate deadlock.
  2. Heap Corruption: In single-threaded builds or non-locking allocators, calling malloc() while an allocation is in flight causes the handler to manipulate half-updated pointers in the heap's free lists or chunk headers. This corrupts heap structures, leading to segmentation faults (SIGSEGV) or silent memory corruption that manifests much later.

Why Calling printf is Dangerous

printf() provides formatted output using the standard I/O library (stdio), which relies on buffered streams (FILE *).

  1. Deadlocks on Stream Locks: Standard I/O streams maintain thread safety via recursive or non-recursive stream locks (such as flockfile()). If a signal interrupts code that is writing to stdout while the stream's lock is held, calling printf() from within the handler will block on that same lock, permanently freezing the application.
  2. Buffer Inconsistency: Standard I/O maintains an internal memory buffer to reduce the frequency of system calls. If printf() is interrupted mid-write, the buffer's write pointers and data indexes are inconsistent. Calling printf() from the signal handler can overwrite uncommitted output, double-flush buffers, or attempt invalid memory access when calculating offsets, causing garbage output or process crashes.

Safe Alternatives

To safely handle signals in C:

  • Flag Setting: The recommended approach is to modify a flag of type volatile sig_atomic_t (or atomic_flag in C11) inside the handler and defer all processing, including memory allocation and logging, to the main loop.
  • Low-Level System Calls: If output is strictly necessary within a signal handler, use direct, unbuffered, async-signal-safe system calls such as write() using file descriptor STDERR_FILENO (2) instead of printf().
  • Self-Pipe Trick: Write a single byte over a non-blocking pipe or eventfd from within the signal handler to wake up a select(), poll(), or epoll() loop running in the main thread.