Permalink
Browse files

nbd-server: fix unsafe signal handling

Previously, signal handlers themselves were entered only once, but they called
posixly unsafe, non-reentrant, functions, such as syslog(). If a signal was
caught in the middle of the execution of such function, consequences were
undefined. In practice, nbd-server was observed to deadlock during the execution
of sigchld_handler() with following kind of system call trace:

  sendto(7, "<31>Mar 30 15:26:26 nbd_server[1"..., 59, MSG_NOSIGNAL, NULL, 0) = 59
  --- SIGCHLD (Child exited) @ 0 (0) ---
  wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = 14367
  futex(0x7f8881903734, FUTEX_WAIT_PRIVATE, 2, NULL

Unsafe signal handlers made nbd-server vulnerable to denial-of-service
attacks. The vulnerability is identified as

  CVE-2015-0847 nbd-server denial of service due to unsafe signal handlers

This patch fixes the problem by moving all signal handling logic away from the
signal handler context to the main loop. Signal handlers just set atomic flags
whenever a signal is received and hence are safe to call at any point.

Furthermore, select() is replaced with pselect() to avoid race conditions with
signal flag tests.

Signed-off-by: Tuomas Räsänen <tuomasjjrasanen@tjjr.fi>
Signed-off-by: Wouter Verhelst <w@uter.be>
  • Loading branch information...
tuomasjjrasanen authored and yoe committed May 7, 2015
1 parent f7bf431 commit 412defe42d03be842c80d21dccf405c435b18432
Showing with 82 additions and 26 deletions.
  1. +82 −26 nbd-server.c
View
@@ -168,6 +168,16 @@ char default_authname[] = SYSCONFDIR "/nbd-server/allow"; /**< default name of a
#include <nbdsrv.h>
static volatile sig_atomic_t is_sigchld_caught; /**< Flag set by
SIGCHLD handler
to mark a child
exit */
static volatile sig_atomic_t is_sigterm_caught; /**< Flag set by
SIGTERM handler
to mark a exit
request */
static volatile sig_atomic_t is_sighup_caught; /**< Flag set by SIGHUP
handler to mark a
reconfiguration
@@ -930,27 +940,16 @@ GArray* parse_cfile(gchar* f, struct generic_conf *const genconf, bool expect_ge
}
/**
* Signal handler for SIGCHLD
* Handle SIGCHLD by setting atomically a flag which will be evaluated in the
* main loop of the root server process. This allows us to separate the signal
* catching from th actual task triggered by SIGCHLD and hence processing in the
* interrupt context is kept as minimial as possible.
*
* @param s the signal we're handling (must be SIGCHLD, or something
* is severely wrong)
**/
void sigchld_handler(int s) {
int status;
int* i;
pid_t pid;
while((pid=waitpid(-1, &status, WNOHANG)) > 0) {
if(WIFEXITED(status)) {
msg(LOG_INFO, "Child exited with %d", WEXITSTATUS(status));
}
i=g_hash_table_lookup(children, &pid);
if(!i) {
msg(LOG_INFO, "SIGCHLD received for an unknown child with PID %ld", (long)pid);
} else {
DEBUG("Removing %d from the list of children", pid);
g_hash_table_remove(children, &pid);
}
}
static void sigchld_handler(const int s G_GNUC_UNUSED) {
is_sigchld_caught = 1;
}
/**
@@ -968,15 +967,16 @@ void killchild(gpointer key, gpointer value, gpointer user_data) {
}
/**
* Handle SIGTERM and dispatch it to our children
* Handle SIGTERM by setting atomically a flag which will be evaluated in the
* main loop of the root server process. This allows us to separate the signal
* catching from th actual task triggered by SIGTERM and hence processing in the
* interrupt context is kept as minimial as possible.
*
* @param s the signal we're handling (must be SIGTERM, or something
* is severely wrong).
**/
void sigterm_handler(int s) {
g_hash_table_foreach(children, killchild, NULL);
unlink(pidfname);
exit(EXIT_SUCCESS);
static void sigterm_handler(const int s G_GNUC_UNUSED) {
is_sigterm_caught = 1;
}
/**
@@ -2065,9 +2065,12 @@ spawn_child()
goto out;
}
/* Child */
/* Child's signal disposition is reset to default. */
signal(SIGCHLD, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGHUP, SIG_DFL);
sigemptyset(&oldset);
out:
sigprocmask(SIG_SETMASK, &oldset, NULL);
return pid;
@@ -2261,9 +2264,12 @@ handle_oldstyle_connection(GArray *const servers, SERVER *const serve)
goto handle_connection_out;
}
/* child */
/* Child's signal disposition is reset to default. */
signal(SIGCHLD, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGHUP, SIG_DFL);
sigemptyset(&oldset);
sigprocmask(SIG_SETMASK, &oldset, NULL);
g_hash_table_destroy(children);
@@ -2367,6 +2373,8 @@ void serveloop(GArray* servers) {
int max;
fd_set mset;
fd_set rset;
sigset_t blocking_mask;
sigset_t original_mask;
/*
* Set up the master fd_set. The set of descriptors we need
@@ -2389,7 +2397,56 @@ void serveloop(GArray* servers) {
FD_SET(sock, &mset);
max=sock>max?sock:max;
}
/* Construct a signal mask which is used to make signal testing and
* receiving an atomic operation to ensure no signal is received between
* tests and blocking pselect(). */
if (sigemptyset(&blocking_mask) == -1)
err("failed to initialize blocking_mask: %m");
if (sigaddset(&blocking_mask, SIGCHLD) == -1)
err("failed to add SIGCHLD to blocking_mask: %m");
if (sigaddset(&blocking_mask, SIGHUP) == -1)
err("failed to add SIGHUP to blocking_mask: %m");
if (sigaddset(&blocking_mask, SIGTERM) == -1)
err("failed to add SIGTERM to blocking_mask: %m");
if (sigprocmask(SIG_BLOCK, &blocking_mask, &original_mask) == -1)
err("failed to block signals: %m");
for(;;) {
if (is_sigterm_caught) {
is_sigterm_caught = 0;
g_hash_table_foreach(children, killchild, NULL);
unlink(pidfname);
exit(EXIT_SUCCESS);
}
if (is_sigchld_caught) {
int status;
int* i;
pid_t pid;
is_sigchld_caught = 0;
while ((pid=waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
msg(LOG_INFO, "Child exited with %d", WEXITSTATUS(status));
}
i = g_hash_table_lookup(children, &pid);
if (!i) {
msg(LOG_INFO, "SIGCHLD received for an unknown child with PID %ld", (long)pid);
} else {
DEBUG("Removing %d from the list of children", pid);
g_hash_table_remove(children, &pid);
}
}
}
/* SIGHUP causes the root server process to reconfigure
* itself and add new export servers for each newly
* found export configuration group, i.e. spawn new
@@ -2424,8 +2481,7 @@ void serveloop(GArray* servers) {
}
memcpy(&rset, &mset, sizeof(fd_set));
if(select(max+1, &rset, NULL, NULL, NULL)>0) {
if (pselect(max + 1, &rset, NULL, NULL, NULL, &original_mask) > 0) {
DEBUG("accept, ");
for(i=0; i < modernsocks->len; i++) {
int sock = g_array_index(modernsocks, int, i);

0 comments on commit 412defe

Please sign in to comment.