Skip to content

VNX-828: Signal Handler with Functionality that is not Asynchronous-Safe

Reports each line of a C or C++ signal handler that calls a function outside the POSIX async-signal-safe list (stdio, the heap, syslog, exit, localtime, pthread locking, std::cout, new/delete), directly or through a helper defined in the same file.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-828
Severity Medium
SARIF level warning
Kind sast
Languages C, C++
CWE CWE-828
CAPEC CAPEC-97
ATT&CK T1557
CWSS vector CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C
Tags signal-handler async-signal-safety cwe-828 c cpp
Source internal/sast/rules/vnx-828.rego

Overview

This rule reports each line of a C or C++ signal handler that calls a function that is not async-signal-safe. A handler is a function the same file installs with signal(), std::signal, sigset or bsd_signal, or assigns to a struct sigaction’s sa_handler/sa_sigaction, and whose definition is in the file. This maps to CWE-828: Signal Handler with Functionality that is not Asynchronous-Signal-Safe.

Reported calls: stdio (printf, fprintf, snprintf, puts, fflush, fopen, fclose), the heap (malloc, calloc, realloc, free, strdup), syslog, exit/atexit, strtok, strerror, localtime/gmtime/ctime/strftime, getenv/setenv, rand, longjmp, pthread mutex and condition calls, dlopen, system/popen, getaddrinfo, std::to_string, C++ stream output, a std::lock_guard/unique_lock/scoped_lock taking a mutex, new and delete. A call to a function defined in the same file is reported when that function’s own body makes one of these calls (one level deep), so cleanup() that calls fclose is reported at the call in the handler.

Not reported: the async-signal-safe functions (write, read, _exit, abort, kill, raise, sigaction, getpid, close, unlink, memcpy, strlen, siglongjmp), helpers whose bodies make no unsafe call, functions defined in other files, atomics and wrappers whose names say they are signal-safe, upper-case macros, member calls, handlers installed only for synchronous fault signals (SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGABRT, SIGTRAP, SIGSYS; these last-resort crash reporters are reported by VNX-479), SIG_IGN/SIG_DFL, lambdas, comments and string literals. This rule owns the unsafe calls of every handler installed for an asynchronous signal; VNX-479 reports only crash handlers installed solely for fault signals.

Why This Matters

Signal handlers interrupt the main program at an arbitrary point. If the signal arrives while the main thread is inside malloc (holding the heap lock), and the signal handler also calls malloc, a deadlock occurs. Similarly, printf uses FILE stream locks that may be held, causing a deadlock or corrupted output. These bugs cause random crashes, deadlocks, or data corruption that are extremely difficult to reproduce and diagnose.

What Gets Flagged

// FLAGGED: printf in signal handler
void sigint_handler(int sig) {
    printf("Caught signal %d\n", sig);  // not async-signal-safe
    exit(1);  // not async-signal-safe
}
signal(SIGINT, sigint_handler);
// FLAGGED: malloc/free in signal handler
void sigterm_handler(int sig) {
    char *msg = malloc(128);  // not async-signal-safe
    sprintf(msg, "terminating");
    free(msg);
}
sigaction(SIGTERM, &sa, NULL);
// FLAGGED: syslog in signal handler
void sigusr1_handler(int sig) {
    syslog(LOG_INFO, "received SIGUSR1");  // not async-signal-safe
}

Remediation

// SAFE: use volatile sig_atomic_t flag and handle in main loop
#include <signal.h>
#include <stdatomic.h>

static volatile sig_atomic_t got_sigint = 0;
static volatile sig_atomic_t got_sigterm = 0;

void sigint_handler(int sig) {
    got_sigint = 1;  // async-signal-safe: simple assignment
}

void sigterm_handler(int sig) {
    got_sigterm = 1;
}

int main(void) {
    signal(SIGINT, sigint_handler);
    signal(SIGTERM, sigterm_handler);
    
    while (1) {
        if (got_sigint) {
            printf("Caught SIGINT\n");  // safe: in main, not handler
            got_sigint = 0;
            break;
        }
        if (got_sigterm) {
            printf("Caught SIGTERM, cleaning up\n");
            got_sigterm = 0;
            cleanup();
            break;
        }
        // ... normal work
    }
    return 0;
}
// SAFE: use async-signal-safe write() instead of printf
void sigpipe_handler(int sig) {
    const char msg[] = "SIGPIPE received\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);  // write() is async-signal-safe
}

The only async-signal-safe operations in a signal handler are: setting a volatile sig_atomic_t flag, calling _exit() (not exit()), using write() with a pre-allocated buffer, and other functions explicitly listed in POSIX as async-signal-safe.

References

Suppressing this rule

Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):

# nosec vnx-828
// nosec vnx-828

Or record an ignore rule: vulnetix ignore add --rule vnx-828 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-828.rego, cli/internal/sast/docs/vnx-828.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.