Signal handlers can be interrupted by their own signals. If a signal is not reset before its handler is called, this means the handler can essentially 'interrupt itself'. A handler that always successfully executes its code despite interrupting itself is said to be re-entrant.
Some platforms provide the ability to mask signals while a signal handler is being processed. If a signal is masked while its own handler is processed, the handler is un-interruptible, and need not be re-entrant.
Vulnerabilities can arise if a non-re-entrant signal handler is interrupted with its own signal, especially if it manipulates globally-accessible dataIt is possible to safely use the same handler for multiple signals, but doing so increases the likelihood of a security vulnerability. The delivered signal is masked and is not delivered until the registered signal handler exits. However, if this same handler is registered to handle a different signal, execution of the handler may be interrupted by this new signal. If a signal handler is constructed with the expectation that it cannot be interrupted, a vulnerability might exist. To eliminate this attack vector, each signal handler should be registered to handle only one type of signal.
Non-Compliant Coding Example
This non-compliant program registers a single signal handler to process both SIGUSR1
and SIGUSR2
. The variable sig2
should be set to one if one or more SIGUSR1
signals are followed by SIGUSR2
. This code essentially implements a finite state machine within the signal handler.
Code Block | ||
---|---|---|
| ||
#include <signal.h> volatile sig_atomic_t sig1 = 0; volatile sig_atomic_t sig2 = 0; void handler(int signum) { if (signum == SIGUSR1) { sig1 = 1; } else if (sig1) { sig2 = 1; } } int main(void) { signal(SIGUSR1, handler); signal(SIGUSR2, handler); while (sig2 == 0) { /* do nothing or give up CPU for a while */ } /* ... */ return 0; } |
The problem with this code is that there is a race condition in the implementation of handler()
. If handler()
is called to handle SIGUSR1
and is interrupted to handle SIGUSR2
, it is possible that sig2
will not be set.
This non-compliant code example also violates SIG31-C. Do not access or modify shared objects in signal handlers.
Compliant Solution
This compliant solution registers two separate signal handlers to process SIGUSR1
and SIGUSR2
. The sig1_handler()
handler waits for SIGUSER1
. After this signal occurs, the sig2_handler()
is registered to handle SIGUSER2
. This solution is fully compliant and accomplishes the goal of detecting whether one or more SIGUSR1
signals are followed by SIGUSR2
.moves the finite state machine out of the signal handler, making it re-entrant.
Code Block | ||
---|---|---|
| ||
#include <signal.h> volatile sig_atomic_t sig1 = 0; volatile sig_atomic_t sig2 = 0; void sig1_handler(int signum) { if (signum == SIGUSR1) { sig1 = 1; } void sig2_handler(int signum else if (signum == SIGUSR2) { sig2 = 1; } } int main(void) { int state = 0; signal(SIGUSR1, sig1_handler); signal(SIGUSR2, SIG_IGNhandler); while (sig1state =!= 02) { /* do nothing or give up CPU for a while */ if (state == 0 && sig1) { state = 1; } if (state == 1 && sig2) { state = 2; } } /* ... */ return signal(SIGUSR2, sig2_handler);0; } |
Compliant Solution (POSIX)
POSIX defines the sigaction(2)
function, which assigns handlers to signals like signal(2)
, but also allows one to explicitly set signal masks. One can thus use sigaction(2)
and prevent a signal handler from interrupting itself.
Code Block | ||
---|---|---|
| ||
#include <signal.h>
#include <stdio.h>
volatile sig_atomic_t sig1 = 0;
volatile sig_atomic_t sig2 = 0;
void handler(int signum) {
if (signum == SIGUSR1) {
sig1 = 1;
}
else if (sig1) {
sig2 = 1;
}
}
int main(void) {
struct sigaction act;
act.sa_handler = &handler;
act.sa_flags = 0;
if (sigfillset( &act.sa_mask) != 0) {
/* handle error */
}
if (sigdelset( &act.sa_mask, SIGUSR1)) {
/* handle error */
}
if (sigdelset( &act.sa_mask, SIGUSR2)) {
/* handle error */
}
if (sigaction(SIGUSR1, &act, NULL) != 0) {
/* handle error */
}
if (sigaction(SIGUSR2, &act, NULL) != 0) {
/* handle error */
}
while (sig2 == 0) {
/* do nothing or give up CPU for a while */
}
/* ... */
return 0;
}
|
In fact, POSIX recommends sigaction(2)
and deprecates signal(2)
. Unfortunately, sigaction(2)
is not C99-compliant.
Risk Assessment
Depending on the code, this could lead to any number of attacks, many of which could give root access. For an overview of some software vulnerabilities, see Zalewski's signal article.
Recommendation | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
SIG00-A | 3 (high) | 3 (likely) | 1 (high) | P9 | L2 |
Automated Detection
...
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
References
Wiki Markup |
---|
\[[ISO/IEC 03|AA. C References#ISO/IEC 03]\] Section 5.2.3, "Signals and interrupts" \[[Open Group 04|AA. C References#Open Group 04]\] [longjmp|http://www.opengroup.org/onlinepubs/000095399/functions/longjmp.html] \[OpenBSD\] [{{signal()}} Man Page|http://www.openbsd.org/cgi-bin/man.cgi?query=signal] \[Zalewski\] [http://lcamtuf.coredump.cx/signals.txt] \[[Dowd 06 | AA. C References#Dowd 06]\] Chapter 13, "Synchronization and State" (Signal Interruption and Repetition) |
...