Skip to content

CWE-364: Signal Handler Race Condition

Overview

Signal handler race conditions occur when a signal handler accesses shared state (global data, static variables) or calls non-reentrant functions. Because a signal can interrupt normal execution - or another handler - at essentially any point, code that assumes it runs to completion without interruption can end up with corrupted data, deadlocks, or double-frees.

Relationship to Other CWEs

Risk

High: Signal handler races corrupt data when a handler interrupts a partially completed update, deadlock when a handler acquires a lock or calls a non-async-signal-safe function such as malloc or printf, and free the same memory twice when a handler re-enters code that was mid-deallocation. The resulting crashes are hard to debug and reproduce.

Remediation Steps

Core Principle: A signal handler can interrupt anything, at any point; treat it as untrusted concurrent code and restrict what it's allowed to touch.

Trace the Data Path

  • Source: Any signal handler registered by the program
  • Sink: Shared state or non-reentrant functions the handler accesses or calls
  • Missing control: No restriction on what the handler touches, and no synchronization between the handler and the code it can interrupt

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

// SECURE - pseudo-code
handler(signal):
    flag = 1   // atomic write to a sig_atomic_t-equivalent, nothing else

main_loop():
    if flag:
        handle_signal()   // all real work happens here, in normal execution context
        flag = 0

Do the minimum possible work inside the handler - typically just setting a flag - and perform all real processing in the normal execution context afterward, where memory allocation, I/O, and locking are all safe again.

Use Atomic, Volatile Shared State (Defense in Depth)

Any variable a handler writes and normal code reads (or vice versa) must be a type the platform guarantees atomic access to, and must be marked so the compiler can't cache or reorder around it. Without both, a read can observe a partially-written value, or never observe the update at all.

Block Signals During Critical Sections

Where a multi-step update to shared state can't be avoided, block the relevant signals for the duration of that update so the handler cannot interrupt it, then restore the previous signal mask - restore rather than unblock, so the section nests inside a caller that had already blocked something.

Two properties of blocking change what the code after it may assume. Delivery is deferred, not counted: on POSIX, several sends of the same standard signal while it is blocked produce one delivery when the mask is restored, so the handler must be written to reconcile state rather than to process one event per signal. And the mask belongs to a thread, not to the process, so in a threaded program blocking a signal on one thread leaves every other thread eligible to receive it.

Register Handlers With Defined Semantics

Where the platform offers more than one way to install a handler, use the one with specified behaviour rather than the convenient one, and use it to say which signals are blocked while the handler runs. A handler serving several signals can be re-entered by a sibling signal even when the delivered one is blocked for it, and that re-entry is the usual route from this weakness to a double free. MITRE tracks that shape separately as CWE-831.

Test the Fix

  • Send the relevant signals during execution, including rapid/repeated delivery, to expose timing-dependent bugs. Where one handler serves several signals, send two of them back to back - that is the case a single-signal test never reaches
  • 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 for it 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 functions that aren't guaranteed async-signal-safe, following the transitive calls rather than only the ones written in the handler
  • 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 signal-safe - can deadlock or corrupt the heap
    format_message(buffer, signal)
    free(buffer)

Why this is vulnerable: the handler and the code it interrupted share everything - the same address space, the same globals, the same thread - and nothing coordinates them. A signal arrives between two machine instructions rather than between two statements, so there is no point in the interrupted code that is safe by construction: a struct half updated, a pointer half written on an architecture where that store is not atomic, an allocator's free list mid-relink.

Two things follow that the code does not show. The window is not small in the way "unlikely race" suggests: whoever can trigger the signal chooses when it arrives and can retry indefinitely until it lands where they want. And the handler can interrupt itself. A second delivery of the same signal does this only where the platform is not blocking it for the duration, which depends on how the handler was registered - that is why the registration choice is a control rather than a style question. A different signal sharing the handler, or a second handler touching the same state, re-enters code already partway through no matter how careful the registration was, unless the sibling was named as blocked too. What a handler can safely touch is therefore not "data protected by a lock" but data the platform guarantees to read and write atomically, and only then if the compiler has been told the value can change outside the current flow.

Secure Patterns

// SECURE - pseudo-code
handler(signal):
    write(stderr_fd, "signal received\n")   // async-signal-safe system call, no allocation
    set_flag(signal)

Why this works: The handler is restricted to operations POSIX (or the equivalent platform guarantee) documents as safe to call from within a signal handler - raw system calls with no internal buffering, locking, or allocation. Anything that needs a lock, allocates memory, or uses non-reentrant library state is deferred to the normal execution context, where none of those restrictions apply.

Language-Specific Guidance

  • C - async-signal-safe function list, sig_atomic_t, sigaction versus signal, sigprocmask, the self-pipe trick for event-loop integration

Additional Resources