Instead of using a array to store received signals, open a pipe and

have the signal handler write the signal number to one end and
select() on the other end.  This makes it possible to handle signals
similar to I/O without race conditions.
This commit is contained in:
Todd C. Miller
2010-09-10 11:20:32 -04:00
parent f601085de4
commit 59399d55c3
3 changed files with 239 additions and 85 deletions

View File

@@ -60,9 +60,21 @@
#include "sudo_plugin.h"
#include "sudo_plugin_int.h"
/* shared with exec_pty.c */
sig_atomic_t recvsig[NSIG];
void handler(int s);
/* Shared with exec_pty.c for use with handler(). */
int signal_pipe[2];
/* We keep a tailq of signals to forward to child. */
struct sigforward {
struct sigforward *prev, *next;
int signo;
};
TQ_DECLARE(sigforward)
static struct sigforward_list sigfwd_list;
static int handle_signals(int fd, pid_t child, int log_io,
struct command_status *cstat);
static void forward_signals(int fd);
static void schedule_signal(int signo);
/*
* Like execve(2) but falls back to running through /bin/sh
@@ -113,6 +125,8 @@ static int fork_cmnd(struct command_details *details, char *argv[],
case 0:
/* child */
close(sv[0]);
close(signal_pipe[0]);
close(signal_pipe[1]);
fcntl(sv[1], F_SETFD, FD_CLOEXEC);
if (exec_setup(details, NULL, -1) == TRUE) {
/* headed for execve() */
@@ -142,10 +156,9 @@ int
sudo_execve(struct command_details *details, char *argv[], char *envp[],
struct command_status *cstat)
{
sigaction_t sa;
int log_io, maxfd, n, nready, sv[2];
fd_set *fdsr, *fdsw;
int maxfd, n, nready, status, sv[2];
int log_io;
sigaction_t sa;
pid_t child;
/* If running in background mode, fork and exit. */
@@ -177,6 +190,13 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
if (socketpair(PF_UNIX, SOCK_DGRAM, 0, sv) != 0)
error(1, "cannot create sockets");
/*
* We use a pipe to atomically handle signal notification within
* the select() loop.
*/
if (pipe_nonblock(signal_pipe) != 0)
error(1, "cannot create pipe");
zero_bytes(&sa, sizeof(sa));
sigemptyset(&sa.sa_mask);
@@ -192,7 +212,7 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
sigaction(SIGTERM, &sa, NULL);
/* Max fd we will be selecting on. */
maxfd = sv[0];
maxfd = MAX(sv[0], signal_pipe[0]);
/*
* Child will run the command in the pty, parent will pass data
@@ -223,64 +243,39 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
fdsr = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask));
fdsw = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask));
for (;;) {
if (recvsig[SIGCHLD]) {
pid_t pid;
/*
* If logging I/O, child is the intermediate process,
* otherwise it is the command itself.
*/
recvsig[SIGCHLD] = FALSE;
do {
pid = waitpid(child, &status, WUNTRACED|WNOHANG);
} while (pid == -1 && errno == EINTR);
if (pid == child) {
/* If not logging I/O and child has exited we are done. */
if (!log_io) {
if (WIFSTOPPED(status)) {
/* Child may not have privs to suspend us itself. */
kill(getpid(), WSTOPSIG(status));
} else {
/* Child has exited, we are done. */
cstat->type = CMD_WSTATUS;
cstat->val = status;
return 0;
}
}
/* Else we get ECONNRESET on sv[0] if child dies. */
}
}
zero_bytes(fdsw, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask));
zero_bytes(fdsr, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask));
FD_SET(sv[0], fdsr);
if (!tq_empty(&sigfwd_list))
FD_SET(sv[0], fdsw);
FD_SET(signal_pipe[0], fdsr);
if (log_io)
fd_set_iobs(fdsr, fdsw); /* XXX - better name */
for (n = 0; n < NSIG; n++) {
if (recvsig[n] && n != SIGCHLD) {
if (log_io) {
FD_SET(sv[0], fdsw);
break;
} else {
/* nothing listening on sv[0], send directly */
if (n == SIGALRM) {
terminate_child(child, FALSE);
} else {
kill(child, n);
}
}
}
}
if (recvsig[SIGCHLD])
continue;
nready = select(maxfd + 1, fdsr, fdsw, NULL, NULL);
if (nready == -1) {
if (errno == EINTR)
continue;
error(1, "select failed");
}
if (FD_ISSET(sv[0], fdsw)) {
forward_signals(sv[0]);
}
if (FD_ISSET(signal_pipe[0], fdsr)) {
n = handle_signals(signal_pipe[0], child, log_io, cstat);
if (n == 0) {
/* Child has exited, cstat is set, we are done. */
goto done;
}
if (n == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
/* Error reading signal_pipe[0], should not happen. */
break;
}
/* Restart event loop so signals get sent to child immediately. */
continue;
}
if (FD_ISSET(sv[0], fdsr)) {
/* read child status */
n = recv(sv[0], cstat, sizeof(*cstat), 0);
@@ -302,7 +297,7 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
/* Suspend parent and tell child how to resume on return. */
sudo_debug(8, "child stopped, suspending parent");
n = suspend_parent(WSTOPSIG(cstat->val));
recvsig[n] = TRUE;
schedule_signal(n);
continue;
} else {
/* Child exited or was killed, either way we are done. */
@@ -314,23 +309,6 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
}
}
if (FD_ISSET(sv[0], fdsw)) {
for (n = 0; n < NSIG; n++) {
if (!recvsig[n])
continue;
recvsig[n] = FALSE;
sudo_debug(9, "sending signal %d to child over backchannel", n);
cstat->type = CMD_SIGNO;
cstat->val = n;
do {
n = send(sv[0], cstat, sizeof(*cstat), 0);
} while (n == -1 && errno == EINTR);
if (n != sizeof(*cstat)) {
recvsig[n] = TRUE;
break;
}
}
}
if (perform_io(fdsr, fdsw, cstat) != 0)
break;
}
@@ -348,18 +326,173 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
}
#endif
done:
efree(fdsr);
efree(fdsw);
while (!tq_empty(&sigfwd_list)) {
struct sigforward *sigfwd = tq_first(&sigfwd_list);
tq_remove(&sigfwd_list, sigfwd);
efree(sigfwd);
}
return cstat->type == CMD_ERRNO ? -1 : 0;
}
/*
* Read signals on fd written to by handler().
* Returns -1 on error (possibly non-fatal), 0 on child exit, else 1.
*/
static int
handle_signals(int fd, pid_t child, int log_io, struct command_status *cstat)
{
unsigned char signo;
ssize_t nread;
int status;
pid_t pid;
/* read signal pipe */
nread = read(signal_pipe[0], &signo, sizeof(signo));
if (nread <= 0) {
/* It should not be possible to get EOF but just in case. */
if (nread == 0)
errno = ECONNRESET;
if (errno != EINTR && errno != EAGAIN) {
sudo_debug(9, "error reading signal pipe %s", strerror(errno));
cstat->type = CMD_ERRNO;
cstat->val = errno;
}
return -1;
}
sudo_debug(9, "received signal %d", signo);
if (signo == SIGCHLD) {
/*
* If logging I/O, child is the intermediate process,
* otherwise it is the command itself.
*/
do {
pid = waitpid(child, &status, WUNTRACED|WNOHANG);
} while (pid == -1 && errno == EINTR);
if (pid == child) {
/* If not logging I/O and child has exited we are done. */
if (!log_io) {
if (WIFSTOPPED(status)) {
/* Child may not have privs to suspend us itself. */
kill(getpid(), WSTOPSIG(status));
} else {
/* Child has exited, we are done. */
cstat->type = CMD_WSTATUS;
cstat->val = status;
return 0;
}
}
/* Else we get ECONNRESET on sv[0] if child dies. */
}
} else {
if (log_io) {
/* Schedule signo to be forwared to the child. */
schedule_signal(signo);
} else {
/* Nothing listening on sv[0], send directly. */
if (signo == SIGALRM) {
terminate_child(child, FALSE);
} else {
kill(child, signo);
}
}
}
return 1;
}
/*
* Forward signals in sigfwd_list to child listening on fd.
*/
static void
forward_signals(int sock)
{
struct sigforward *sigfwd;
struct command_status cstat;
ssize_t nsent;
while (!tq_empty(&sigfwd_list)) {
sigfwd = tq_first(&sigfwd_list);
sudo_debug(9, "sending signal %d to child over backchannel",
sigfwd->signo);
cstat.type = CMD_SIGNO;
cstat.val = sigfwd->signo;
do {
nsent = send(sock, &cstat, sizeof(cstat), 0);
} while (nsent == -1 && errno == EINTR);
tq_remove(&sigfwd_list, sigfwd);
efree(sigfwd);
if (nsent != sizeof(cstat)) {
if (errno == EPIPE) {
/* Other end of socket gone, empty out sigfwd_list. */
while (!tq_empty(&sigfwd_list)) {
sigfwd = tq_first(&sigfwd_list);
tq_remove(&sigfwd_list, sigfwd);
efree(sigfwd);
}
}
break;
}
}
}
/*
* Schedule a signal to be forwared.
*/
static void
schedule_signal(int signo)
{
struct sigforward *sigfwd;
sigfwd = emalloc(sizeof(*sigfwd));
sigfwd->prev = sigfwd;
sigfwd->next = NULL;
sigfwd->signo = signo;
tq_append(&sigfwd_list, sigfwd);
}
/*
* Generic handler for signals passed from parent -> child.
* The recvsig[] array is checked in the main event loop.
* The other end of signal_pipe is checked in the main event loop.
*/
void
handler(int s)
{
recvsig[s] = TRUE;
unsigned char signo = (unsigned char)s;
/*
* The pipe is non-blocking, if we overflow the kernel's pipe
* buffer we drop the signal. This is not a problem in practice.
*/
(void)write(signal_pipe[1], &signo, sizeof(signo));
}
/*
* Open a pipe and make both ends non-blocking.
* Returns 0 on success and -1 on error.
*/
int
pipe_nonblock(int fds[2])
{
int flags, rval;
rval = pipe(fds);
if (rval != -1) {
flags = fcntl(fds[0], F_GETFL, 0);
if (flags != -1 && !ISSET(flags, O_NONBLOCK))
rval = fcntl(fds[0], F_SETFL, flags | O_NONBLOCK);
if (rval != -1) {
flags = fcntl(fds[1], F_GETFL, 0);
if (flags != -1 && !ISSET(flags, O_NONBLOCK))
rval = fcntl(fds[1], F_SETFL, flags | O_NONBLOCK);
}
if (rval == -1) {
close(fds[0]);
close(fds[1]);
}
}
return rval;
}