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.
| 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
- CWE-828: Signal Handler with Functionality that is not Asynchronous-Signal-Safe
- POSIX Async-Signal-Safe Functions
- CERT SIG30-C: Call only asynchronous-safe functions within signal handlers
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.