[ Team LiB ] |
13.5 Performing Proper Signal Handling13.5.1 ProblemYour program needs to handle asynchronous signals. 13.5.2 SolutionOn Unix systems, it is often necessary to perform some amount of signal handling. In particular, if a program receives a termination signal, it is often desirable to perform some kind of cleanup before terminating the program—flushing in-memory caches to disk, recording the event to a log file, and so on. Unfortunately, many programmers do not perform their signal handling safely, which of course leads to possible security vulnerabilities. Even more unfortunate is that there is no cookie-cutter solution to writing safe signal handlers. Fortunately, following some easy guidelines will help you write more secure signal-handling code.
If you must perform more complex operations in a signal handler than we are recommending here, you should block signal delivery during any nonatomic operations that may be impacted by operations performed in a signal handler. In addition, you should block signal delivery inside all signal handlers. We strongly recommend against performing complex operations in a signal handler. If you feel that it's necessary, be aware that it can be done but is error-prone and will negatively affect program performance. As an example of what you must do to safely use malloc( ) (whether directly or indirectly) from inside a signal handler, note that any time malloc( ) needs to be called inside or outside the signal handler, signal delivery will need to be blocked before the call to malloc( ) and unblocked again after the call. Changing the signal delivery often incurs a context switch from user mode to kernel mode; when such switching is done so frequently, it can quickly add up to a significant decrease in performance. In addition, because you may never be certain which functions may call malloc( ) under the covers, you may need to protect everything, which can easily result in forgotten protections in places. 13.5.3 DiscussionAs we have already mentioned, there is unfortunately no cookie-cutter solution to writing safe signal handlers. The code presented here is simply an example of how signal handlers can be properly written. A much more detailed discussion of signal handling, which includes real-world examples of how improperly written signal handlers can be exploited, can be found in Michal Zalewski's paper, "Delivering Signals for Fun and Profit," which is available at http://www.netsys.com/library/papers/signals.txt. Another excellent source of information regarding the proper way to write signal handlers is Advanced Programming in the Unix Environment by W. Richard Stevens (Addison Wesley). #include <stdio.h> #include <signal.h> #include <unistd.h> int sigint_received = 0; int sigterm_received = 0; int sigquit_received = 0; void handle_sigint(int sig) { sigint_received = 1; } void handle_sigterm(int sig) { sigterm_received = 1; } void handle_sigquit(int sig) { sigquit_received = 1; } static void setup_signal_handler(int sig, void (*handler)( )) { #if _POSIX_VERSION > 198800L struct sigaction action; action.sa_handler = handler; sigemptyset(&(action.sa_mask)); sigaddset(&(action.sa_mask), sig); action.sa_flags = 0; sigaction(sig, &action, 0); #else signal(sig, handler); #endif } static int signal_was_caught(void) { if (sigint_received) printf("SIGINT received!\n"); if (sigterm_received) printf("SIGTERM received!\n"); if (sigquit_received) printf("SIGQUIT received!\n"); return (sigint_received || sigterm_received || sigquit_received); } int main(int argc, char *argv[ ]) { char buffer[80]; setup_signal_handler(SIGINT, handle_sigint); setup_signal_handler(SIGTERM, handle_sigterm); setup_signal_handler(SIGQUIT, handle_sigquit); /* The main loop of this program simply reads input from stdin, and * throws it away. It's useless functionality, but the point is to * illustrate signal handling, and fread is a system call that will * be interrupted by signals, so it works well for example purposes */ while (!feof(stdin)) { fread(buffer, 1, sizeof(buffer), stdin); if (signal_was_caught( )) break; } return (sigint_received || sigterm_received || sigquit_received); } This code clearly illustrates both points made in Section 13.5.2. Separate signal handlers are used for each signal that we want to handle: SIGINT, SIGTERM, and SIGQUIT. For each signal handler, a global flag is set to nonzero to indicate that the signal was caught. Later, when the system call—fread( ) in this case—returns, the flags are checked and fully handled. (It is true that fread( ) itself is not really a system call, but it is a wrapper around the read( ) system call.) In the function setup_signal_handler( ), we use sigaction( ) to set up our signal handlers, rather than signal( ), if it is available. On most modern Unix systems, sigaction( ) is available and should be used. One problem with signal( ) is that on some platforms it is subject to race conditions because it is implemented as a wrapper around sigaction( ). Another problem is that on some systems—most notably those that are BSD-derived—some system calls are restarted when interrupted by a signal, which is typically not the behavior we want. In this particular example, it certainly is not because we won't get the opportunity to check our flags until after the call to fread( ) completes, which could be a long time. Using sigaction( ) without the nonportable SA_RESTART flag will disable this behavior and cause fread( ) to return immediately with the global errno set to EINTR. The function signal_was_caught( ) is used to check each of the signal flags and print an appropriate message if one of the signals was received. It is, in fact, possible that more than one signal could have been received, so all the flags are checked. Immediately after the call to fread( ), we call signal_was_caught( ) to do the signal tests and immediately break out of our loop and exit if any one of the signals was received. 13.5.4 See Also
|
[ Team LiB ] |