Skip to content

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

Overview

Signal handlers can interrupt program execution at any point, including in the middle of a function that isn't safe to re-enter. Calling a non-reentrant function - one that relies on internal locks, static/global buffers, or its own not-yet-consistent state - from a signal handler is unsafe, and POSIX makes it undefined behavior. This is inherent to signal-based programming in C and similar languages. It does not arise in runtimes that never deliver signals asynchronously into user code. POSIX draws the line by naming the functions that are safe rather than by reasoning about reentrancy. POSIX.1-2017 ยง2.4.3 says that in a multi-threaded process - or in a single-threaded one where the handler is not running as a direct result of abort(), raise(), kill(), pthread_kill() or sigqueue() - the behavior is undefined "if the signal handler calls any function defined in this standard other than one of the functions listed in the following table". Those excepted cases are synchronous deliveries the program asked for, where it already knows what it interrupted; they are not a licence worth building on, because a handler installed for SIGINT or SIGTERM is by definition not one of them. Reentrancy is why most functions are absent from that list, but the list is what the rule is keyed to, so the question to ask of a call in a handler is whether it is on it.

Relationship to Other CWEs

CWE-479 sits under CWE-364 (Signal Handler Race Condition) through CWE-828 (Signal Handler with Functionality that is not Asynchronous-Safe) rather than directly. That intermediate number matters only if a tool reports it: CWE-828 has no page here and the guidance would be the same. CWE-364 covers a handler racing with the main program over shared state in general. CWE-479 is specifically about the handler calling a function - malloc, printf, syslog, a mutex lock - that isn't safe to call from within a handler at all, regardless of whether it touches program-specific shared state. MITRE also files it under CWE-663 (Use of a Non-reentrant Function in a Concurrent Context), which is the same defect where the concurrent context is a thread rather than a signal.

The fix for all of them is the same restriction: keep handlers to the documented async-signal-safe function list and defer everything else to normal execution context.

Risk

High: Non-reentrant functions in signal handlers cause deadlocks (acquiring an already-held lock), heap corruption (malloc interrupted mid-allocation), and crashes on data structures the interrupted code left mid-update. They are hard to debug and reproduce, since the failure depends on exactly when the signal arrives.

Remediation Steps

Core Principle: A signal handler may only call functions documented as async-signal-safe; everything else belongs in the normal execution context.

Trace the Data Path

  • Source: Every registered signal handler
  • Sink: Every function that handler calls, directly or indirectly
  • Missing control: No check that each called function is on the platform's async-signal-safe list

Restrict Handlers to Async-Signal-Safe Functions (Primary Defense)

// SECURE - pseudo-code
handler(signal):
    flag = 1   // atomic write only, nothing else

main_loop():
    if flag:
        do_the_real_work()   // malloc, printf, locks - all safe here
        flag = 0

Reduce the handler to the minimum: set a flag, or write a single byte to a pipe the main loop is watching. Perform every non-reentrant operation - memory allocation, formatted output, locking, cleanup - in the normal execution context afterward.

Prefer Handler-Free Signal Delivery Where Available

Some platforms can deliver signals as ordinary events (a readable file descriptor) instead of invoking a handler at all. Where that's available, it removes the async-signal-safety question entirely, since none of the code that processes the signal runs inside a handler.

Test the Fix

  • Send the relevant signal repeatedly under load to expose timing-dependent failures
  • Use a sanitizer for its async-signal-safety report rather than for race detection. A data-race detector will not find this class: the handler runs on the thread it interrupted, so there is no second thread to report a race against. What such tools do catch is the unsafe call - an allocation or a formatted write made from inside a handler
  • Review every handler for calls to allocation, formatted-output, and locking functions, and follow the transitive calls. A handler that calls the program's own logging helper is unsafe if that helper eventually reaches printf
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
handler(signal):
    buffer = allocate(size)      // allocator is not reentrant - can deadlock or corrupt the heap
    format_message(buffer, signal)
    acquire_lock(some_lock)      // can deadlock if the interrupted code already holds it

Why this is vulnerable: a signal handler does not run alongside the program, it runs inside it. The kernel suspends the interrupted thread between two instructions and runs the handler on that same thread, so the handler begins with whatever the program was halfway through still halfway through - a free list partly relinked, a buffer partly flushed, an object partly constructed. Calling back into the code that owns that state re-enters it mid-update.

This is why thread safety is the wrong property to reason about here. A mutex is what makes a function safe to call from two threads, and it is exactly what cannot help when the second caller is the same thread: that thread already holds the lock, it is suspended, and the only code that could release it is waiting for the handler to return. The property that applies is async-signal-safety, which is a much shorter list and is fixed by the platform rather than chosen by the caller. That is why a workable handler does almost nothing and leaves the real work to the main loop.

Secure Patterns

// SECURE - pseudo-code
handler(signal):
    write(stderr_fd, "signal received\n")   // raw system call, no internal state to corrupt
    set_flag(signal)                         // deferred processing happens outside the handler

Why this works: write is on the documented async-signal-safe list - a raw system call with no internal state to corrupt - and setting the flag is a single atomic write rather than a call into code that could already be mid-update. Anything that allocates memory, formats output through a buffered library, or acquires a lock is deferred to the normal execution context.

Language-Specific Guidance

  • C - async-signal-safe function list, minimal-handler and self-pipe patterns, signalfd for handler-free delivery on Linux

Additional Resources