Safeguard
Application Security•

CWE-364 Signal Handler Race Condition in C and C++

CWE-364 is a signal handler that calls unsafe functions or touches shared state, so interrupted code breaks. Learn the pattern and the flag based fix.

Safeguard Security Team
5 min read

CWE-364, Signal Handler Race Condition, is a race condition created by a signal handler. MITRE lists C and C++ as the affected languages. It matters to anyone writing daemons, network services or command line tools that handle signals like SIGTERM, SIGHUP or SIGINT.

How it happens

A signal can arrive at almost any moment. When it does, the operating system pauses whatever the program was doing and runs the handler. When the handler returns, the program resumes.

That is the problem. MITRE explains that signal handlers support asynchronous actions, and races occur when non-reentrant functions or state sensitive actions run inside them. The interrupted code may have been halfway through changing a data structure. The handler then reads or changes the same structure in a state the author never planned for.

MITRE gives a concrete example. If a non-reentrant function is already running when the signal fires, and the handler calls it again, memory corruption could occur that is exploitable for code execution. Another common pattern involves free being called inside a handler. If the signal arrives twice, or arrives while the main code is also freeing or using that memory, the result is a double free or use after free.

The observed examples in the record:

  • CVE-1999-0035: a signal handler did not disable other signal handlers, so it could be interrupted, causing other functionality to access files in unsafe ways.
  • CVE-2001-0905: an attacker could send a signal while another handler was already running, leading to a crash or execution with root privileges.
  • CVE-2001-1349: unsafe calls to library functions from a signal handler.

What an attacker gains

MITRE lists integrity, confidentiality and availability: modifying application data, modifying memory, crashing the process, and executing unauthorized code. Corrupting global variables or data structures at unexpected times violates assumptions the rest of the program makes.

There is also a privilege angle. If a handler interrupts code that is running with elevated privileges, the handler may also run with them, which can make follow up exploits more severe.

The attacker's tool is simple. On many systems a local user can send signals to a process they can influence, or trigger conditions that produce them, and can repeat the attempt until the timing works.

How to find it

MITRE notes that automated static analysis can find some instances of this weakness by analyzing source code. Add a review of every handler.

  • List all calls to signal or sigaction and read each handler function.
  • In each handler, list every function called. Is it on your platform's list of async-signal-safe functions? Calls like malloc, free, printf and most logging functions are not.
  • Check which global variables the handler touches, and how the main code uses them.
  • Check whether the handler can be interrupted by another signal, including the same one.

How to fix it

MITRE's guidance has three parts.

  1. Language selection: where possible, use a language that does not allow this weakness or makes it easier to avoid.
  2. Design handlers to only set flags, rather than perform complex functionality. The main loop checks the flags and does the real work.
  3. Only use reentrant functions in handlers, and validate that state is consistent while performing asynchronous actions that affect execution state.

Here is the vulnerable pattern in C:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

char *buf;

void on_term(int sig) {
    printf("shutting down\n");   /* not async-signal-safe */
    free(buf);                   /* may race with main code */
    exit(0);
}

The fix sets a flag and returns:

#include <signal.h>

static volatile sig_atomic_t stop = 0;

void on_term(int sig) {
    stop = 1;
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = on_term;
    sigaction(SIGTERM, &sa, NULL);

    while (!stop) {
        /* do work */
    }
    /* clean up here, in normal context */
    return 0;
}

The handler does one thing: write a value of the type designed for this purpose. All cleanup happens in the main flow, where it cannot interrupt itself. Use sigaction rather than the older signal function, since its behavior is better specified, and block other signals while the handler runs where that matters.

Quick check

Why is calling free in a signal handler dangerous?

The handler can run while the main code is in the middle of memory operations, or run twice, causing double free or use after free and possibly code execution.

What is the safest handler design?

Only set a flag. Let the main loop notice the flag and do the real work in normal context, which is MITRE's recommended design.

Why can a handler be a privilege problem?

If it interrupts code running with elevated privileges, the handler can also run with them, making later exploits more severe.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.