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.h"
#include "sudo_plugin_int.h" #include "sudo_plugin_int.h"
/* shared with exec_pty.c */ /* Shared with exec_pty.c for use with handler(). */
sig_atomic_t recvsig[NSIG]; int signal_pipe[2];
void handler(int s);
/* 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 * 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: case 0:
/* child */ /* child */
close(sv[0]); close(sv[0]);
close(signal_pipe[0]);
close(signal_pipe[1]);
fcntl(sv[1], F_SETFD, FD_CLOEXEC); fcntl(sv[1], F_SETFD, FD_CLOEXEC);
if (exec_setup(details, NULL, -1) == TRUE) { if (exec_setup(details, NULL, -1) == TRUE) {
/* headed for execve() */ /* headed for execve() */
@@ -142,10 +156,9 @@ int
sudo_execve(struct command_details *details, char *argv[], char *envp[], sudo_execve(struct command_details *details, char *argv[], char *envp[],
struct command_status *cstat) struct command_status *cstat)
{ {
sigaction_t sa; int log_io, maxfd, n, nready, sv[2];
fd_set *fdsr, *fdsw; fd_set *fdsr, *fdsw;
int maxfd, n, nready, status, sv[2]; sigaction_t sa;
int log_io;
pid_t child; pid_t child;
/* If running in background mode, fork and exit. */ /* 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) if (socketpair(PF_UNIX, SOCK_DGRAM, 0, sv) != 0)
error(1, "cannot create sockets"); 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)); zero_bytes(&sa, sizeof(sa));
sigemptyset(&sa.sa_mask); sigemptyset(&sa.sa_mask);
@@ -192,7 +212,7 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
sigaction(SIGTERM, &sa, NULL); sigaction(SIGTERM, &sa, NULL);
/* Max fd we will be selecting on. */ /* 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 * 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)); fdsr = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask));
fdsw = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask)); fdsw = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask));
for (;;) { 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(fdsw, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask));
zero_bytes(fdsr, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask)); zero_bytes(fdsr, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask));
FD_SET(sv[0], fdsr); FD_SET(sv[0], fdsr);
if (!tq_empty(&sigfwd_list))
FD_SET(sv[0], fdsw);
FD_SET(signal_pipe[0], fdsr);
if (log_io) if (log_io)
fd_set_iobs(fdsr, fdsw); /* XXX - better name */ 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); nready = select(maxfd + 1, fdsr, fdsw, NULL, NULL);
if (nready == -1) { if (nready == -1) {
if (errno == EINTR) if (errno == EINTR)
continue; continue;
error(1, "select failed"); 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)) { if (FD_ISSET(sv[0], fdsr)) {
/* read child status */ /* read child status */
n = recv(sv[0], cstat, sizeof(*cstat), 0); 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. */ /* Suspend parent and tell child how to resume on return. */
sudo_debug(8, "child stopped, suspending parent"); sudo_debug(8, "child stopped, suspending parent");
n = suspend_parent(WSTOPSIG(cstat->val)); n = suspend_parent(WSTOPSIG(cstat->val));
recvsig[n] = TRUE; schedule_signal(n);
continue; continue;
} else { } else {
/* Child exited or was killed, either way we are done. */ /* 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) if (perform_io(fdsr, fdsw, cstat) != 0)
break; break;
} }
@@ -348,18 +326,173 @@ sudo_execve(struct command_details *details, char *argv[], char *envp[],
} }
#endif #endif
done:
efree(fdsr); efree(fdsr);
efree(fdsw); 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; 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. * 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 void
handler(int s) 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;
} }

View File

@@ -567,6 +567,8 @@ fork_pty(struct command_details *details, char *argv[], char *envp[],
case 0: case 0:
/* child */ /* child */
close(sv[0]); close(sv[0]);
close(signal_pipe[0]);
close(signal_pipe[1]);
fcntl(sv[1], F_SETFD, FD_CLOEXEC); fcntl(sv[1], F_SETFD, FD_CLOEXEC);
if (exec_setup(details, slavename, io_fds[SFD_SLAVE]) == TRUE) { if (exec_setup(details, slavename, io_fds[SFD_SLAVE]) == TRUE) {
/* Close the other end of the stdin/stdout/stderr pipes and exec. */ /* Close the other end of the stdin/stdout/stderr pipes and exec. */
@@ -797,7 +799,7 @@ handle_sigchld(int backchannel, struct command_status *cstat)
* parent and relays signals from the parent to the command. * parent and relays signals from the parent to the command.
* Returns an error if fork(2) fails, else calls _exit(2). * Returns an error if fork(2) fails, else calls _exit(2).
*/ */
int static int
exec_monitor(struct command_details *details, char *argv[], char *envp[], exec_monitor(struct command_details *details, char *argv[], char *envp[],
int backchannel) int backchannel)
{ {
@@ -807,6 +809,7 @@ exec_monitor(struct command_details *details, char *argv[], char *envp[],
sigaction_t sa; sigaction_t sa;
int errpipe[2], maxfd, n, status; int errpipe[2], maxfd, n, status;
int alive = TRUE; int alive = TRUE;
unsigned char signo;
/* Close unused fds. */ /* Close unused fds. */
if (io_fds[SFD_MASTER] != -1) if (io_fds[SFD_MASTER] != -1)
@@ -814,6 +817,13 @@ exec_monitor(struct command_details *details, char *argv[], char *envp[],
if (io_fds[SFD_USERTTY] != -1) if (io_fds[SFD_USERTTY] != -1)
close(io_fds[SFD_USERTTY]); close(io_fds[SFD_USERTTY]);
/*
* We use a pipe to atomically handle signal notification within
* the select() loop.
*/
if (pipe_nonblock(signal_pipe) != 0)
error(1, "cannot create pipe");
/* Reset SIGWINCH and SIGALRM. */ /* Reset SIGWINCH and SIGALRM. */
zero_bytes(&sa, sizeof(sa)); zero_bytes(&sa, sizeof(sa));
sigemptyset(&sa.sa_mask); sigemptyset(&sa.sa_mask);
@@ -872,6 +882,8 @@ exec_monitor(struct command_details *details, char *argv[], char *envp[],
if (child == 0) { if (child == 0) {
/* We pass errno back to our parent via pipe on exec failure. */ /* We pass errno back to our parent via pipe on exec failure. */
close(backchannel); close(backchannel);
close(signal_pipe[0]);
close(signal_pipe[1]);
close(errpipe[0]); close(errpipe[0]);
fcntl(errpipe[1], F_SETFD, FD_CLOEXEC); fcntl(errpipe[1], F_SETFD, FD_CLOEXEC);
@@ -905,27 +917,20 @@ exec_monitor(struct command_details *details, char *argv[], char *envp[],
} }
/* Wait for errno on pipe, signal on backchannel or for SIGCHLD */ /* Wait for errno on pipe, signal on backchannel or for SIGCHLD */
maxfd = MAX(errpipe[0], backchannel); maxfd = MAX(MAX(errpipe[0], signal_pipe[0]), backchannel);
fdsr = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask)); fdsr = (fd_set *)emalloc2(howmany(maxfd + 1, NFDBITS), sizeof(fd_mask));
zero_bytes(fdsr, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask)); zero_bytes(fdsr, howmany(maxfd + 1, NFDBITS) * sizeof(fd_mask));
zero_bytes(&cstat, sizeof(cstat)); zero_bytes(&cstat, sizeof(cstat));
tv.tv_sec = 0; tv.tv_sec = 0;
tv.tv_usec = 0; tv.tv_usec = 0;
for (;;) { for (;;) {
/* Read child status. */
if (recvsig[SIGCHLD]) {
recvsig[SIGCHLD] = FALSE;
alive = handle_sigchld(backchannel, &cstat);
}
/* Check for signal on backchannel or errno on errpipe. */ /* Check for signal on backchannel or errno on errpipe. */
FD_SET(backchannel, fdsr); FD_SET(backchannel, fdsr);
FD_SET(signal_pipe[0], fdsr);
if (errpipe[0] != -1) if (errpipe[0] != -1)
FD_SET(errpipe[0], fdsr); FD_SET(errpipe[0], fdsr);
maxfd = MAX(errpipe[0], backchannel); maxfd = MAX(MAX(errpipe[0], signal_pipe[0]), backchannel);
if (recvsig[SIGCHLD])
continue;
/* If command exited we just poll, there may be data on errpipe. */ /* If command exited we just poll, there may be data on errpipe. */
n = select(maxfd + 1, fdsr, NULL, NULL, alive ? NULL : &tv); n = select(maxfd + 1, fdsr, NULL, NULL, alive ? NULL : &tv);
if (n <= 0) { if (n <= 0) {
@@ -936,6 +941,21 @@ exec_monitor(struct command_details *details, char *argv[], char *envp[],
error(1, "select failed"); error(1, "select failed");
} }
if (FD_ISSET(signal_pipe[0], fdsr)) {
/* Read child status. */
n = read(signal_pipe[0], &signo, sizeof(signo));
if (n == -1) {
if (errno == EINTR || errno == EAGAIN)
continue;
warning("error reading from signal pipe");
goto done;
}
/* We should only ever get SIGCHLD. */
if (signo == SIGCHLD) {
alive = handle_sigchld(backchannel, &cstat);
continue;
}
}
if (errpipe[0] != -1 && FD_ISSET(errpipe[0], fdsr)) { if (errpipe[0] != -1 && FD_ISSET(errpipe[0], fdsr)) {
/* read errno or EOF from command pipe */ /* read errno or EOF from command pipe */
n = read(errpipe[0], &cstat, sizeof(cstat)); n = read(errpipe[0], &cstat, sizeof(cstat));

View File

@@ -23,6 +23,7 @@
/* exec.c */ /* exec.c */
int my_execve(const char *path, char *const argv[], char *const envp[]); int my_execve(const char *path, char *const argv[], char *const envp[]);
int pipe_nonblock(int fds[2]);
/* exec_pty.c */ /* exec_pty.c */
int fork_pty(struct command_details *details, char *argv[], char *envp[], int fork_pty(struct command_details *details, char *argv[], char *envp[],
@@ -34,6 +35,6 @@ void handler(int s);
void pty_close(struct command_status *cstat); void pty_close(struct command_status *cstat);
void pty_setup(uid_t uid); void pty_setup(uid_t uid);
void terminate_child(pid_t pid, int use_pgrp); void terminate_child(pid_t pid, int use_pgrp);
extern sig_atomic_t recvsig[NSIG]; extern int signal_pipe[2];
#endif /* _SUDO_EXEC_H */ #endif /* _SUDO_EXEC_H */