Imported Upstream version 1.7.6p1
[debian/sudo] / exec.c
diff --git a/exec.c b/exec.c
index 784f90ab40ff80020dc18f2ea12105076042b5c9..19dbad2e4dca52f116c0a32406a9b37e11505af6 100644 (file)
--- a/exec.c
+++ b/exec.c
@@ -18,6 +18,9 @@
 
 #include <sys/types.h>
 #include <sys/param.h>
+#ifdef HAVE_SYS_SYSMACROS_H
+# include <sys/sysmacros.h>
+#endif
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include "sudo.h"
 #include "sudo_exec.h"
 
-/* shared with exec_pty.c */
-sig_atomic_t recvsig[NSIG];
-void handler __P((int s));
+/* Shared with exec_pty.c for use with handler(). */
+int signal_pipe[2];
+
+#ifdef _PATH_SUDO_IO_LOGDIR
+/* 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 void forward_signals __P((int fd));
+static void schedule_signal __P((int signo));
+static int log_io;
+#endif /* _PATH_SUDO_IO_LOGDIR */
+
+static int handle_signals __P((int fd, pid_t child,
+    struct command_status *cstat));
 
 /*
  * Like execve(2) but falls back to running through /bin/sh
@@ -102,7 +120,7 @@ static int fork_cmnd(path, argv, envp, sv, rbac_enabled)
 {
     struct command_status cstat;
     sigaction_t sa;
-    int pid;
+    pid_t child;
 
     zero_bytes(&sa, sizeof(sa));
     sigemptyset(&sa.sa_mask);
@@ -110,15 +128,18 @@ static int fork_cmnd(path, argv, envp, sv, rbac_enabled)
     sa.sa_handler = handler;
     sigaction(SIGCONT, &sa, NULL);
 
-    pid = fork();
-    switch (pid) {
+    child = fork();
+    switch (child) {
     case -1:
        error(1, "fork");
        break;
     case 0:
        /* child */
        close(sv[0]);
+       close(signal_pipe[0]);
+       close(signal_pipe[1]);
        fcntl(sv[1], F_SETFD, FD_CLOEXEC);
+       restore_signals();
        if (exec_setup(rbac_enabled, user_ttypath, -1) == TRUE) {
            /* headed for execve() */
            closefrom(def_closefrom);
@@ -134,7 +155,51 @@ static int fork_cmnd(path, argv, envp, sv, rbac_enabled)
        send(sv[1], &cstat, sizeof(cstat), 0);
        _exit(1);
     }
-    return pid;
+    return child;
+}
+
+static struct signal_state {
+    int signo;
+    sigaction_t sa;
+} saved_signals[] = {
+    { SIGALRM },
+    { SIGCHLD },
+    { SIGCONT },
+    { SIGHUP },
+    { SIGINT },
+    { SIGPIPE },
+    { SIGQUIT },
+    { SIGTERM },
+    { SIGTSTP },
+    { SIGTTIN },
+    { SIGTTOU },
+    { SIGUSR1 },
+    { SIGUSR2 },
+    { -1 }
+};
+
+/*
+ * Save signal handler state so it can be restored before exec.
+ */
+void
+save_signals()
+{
+    struct signal_state *ss;
+
+    for (ss = saved_signals; ss->signo != -1; ss++)
+       sigaction(ss->signo, NULL, &ss->sa);
+}
+
+/*
+ * Restore signal handlers to initial state.
+ */
+void
+restore_signals()
+{
+    struct signal_state *ss;
+
+    for (ss = saved_signals; ss->signo != -1; ss++)
+       sigaction(ss->signo, &ss->sa, NULL);
 }
 
 /*
@@ -152,11 +217,10 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
     int dowait;
     int bgmode;
 {
-    sigaction_t sa;
-    fd_set *fdsr, *fdsw;
-    int maxfd, n, nready, status, sv[2];
+    int maxfd, n, nready, sv[2];
     int rbac_enabled = 0;
-    int log_io;
+    fd_set *fdsr, *fdsw;
+    sigaction_t sa;
     pid_t child;
 
     /* If running in background mode, fork and exit. */
@@ -200,31 +264,44 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
        my_execve(path, argv, envp);
        cstat->type = CMD_ERRNO;
        cstat->val = errno;
-       return(127);
+       return 127;
     }
 
     /*
      * We communicate with the child over a bi-directional pair of sockets.
      * Parent sends signal info to child and child sends back wait status.
      */
-    if (socketpair(PF_UNIX, SOCK_DGRAM, 0, sv) != 0)
+    if (socketpair(PF_UNIX, SOCK_DGRAM, 0, sv) == -1)
        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);
 
-    /* Note: HP-UX select() will not be interrupted if SA_RESTART set */
+    /*
+     * Signals for forward to the child process (excluding SIGCHLD).
+     * Note: HP-UX select() will not be interrupted if SA_RESTART set.
+     */
     sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
     sa.sa_handler = handler;
+    sigaction(SIGALRM, &sa, NULL);
     sigaction(SIGCHLD, &sa, NULL);
     sigaction(SIGHUP, &sa, NULL);
     sigaction(SIGINT, &sa, NULL);
     sigaction(SIGPIPE, &sa, NULL);
     sigaction(SIGQUIT, &sa, NULL);
     sigaction(SIGTERM, &sa, NULL);
+    sigaction(SIGUSR1, &sa, NULL);
+    sigaction(SIGUSR2, &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
@@ -253,72 +330,48 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
     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 {
-#ifdef sudo_waitpid
-               pid = sudo_waitpid(child, &status, WUNTRACED|WNOHANG);
-#else
-               pid = wait(&status);
-#endif
-           } 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(signal_pipe[0], fdsr);
        FD_SET(sv[0], fdsr);
 #ifdef _PATH_SUDO_IO_LOGDIR
+       if (!tq_empty(&sigfwd_list))
+           FD_SET(sv[0], fdsw);
        if (log_io)
            fd_set_iobs(fdsr, fdsw); /* XXX - better name */
 #endif
-       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 */
-                   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");
        }
+#ifdef _PATH_SUDO_IO_LOGDIR
+       if (FD_ISSET(sv[0], fdsw)) {
+           forward_signals(sv[0]);
+       }
+#endif /* _PATH_SUDO_IO_LOGDIR */
+       if (FD_ISSET(signal_pipe[0], fdsr)) {
+           n = handle_signals(signal_pipe[0], child, cstat);
+           if (n == 0) {
+               /* Child has exited, cstat is set, we are done. */
+               goto done;
+           }
+           if (n == -1) {
+               /* 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);
            if (n == -1) {
                if (errno == EINTR)
                    continue;
+#ifdef _PATH_SUDO_IO_LOGDIR
                /*
                 * If not logging I/O we will receive ECONNRESET when
                 * the command is executed.  It is safe to ignore this.
@@ -328,13 +381,14 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
                    cstat->val = errno;
                    break;
                }
+#endif
            }
 #ifdef _PATH_SUDO_IO_LOGDIR /* XXX */
            if (cstat->type == CMD_WSTATUS) {
                if (WIFSTOPPED(cstat->val)) {
                    /* Suspend parent and tell child how to resume on return. */
                    n = suspend_parent(WSTOPSIG(cstat->val));
-                   recvsig[n] = TRUE;
+                   schedule_signal(n);
                    continue;
                } else {
                    /* Child exited or was killed, either way we are done. */
@@ -349,25 +403,12 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
        }
 
 #ifdef _PATH_SUDO_IO_LOGDIR
-       /* XXX - move this too */
-       if (FD_ISSET(sv[0], fdsw)) {
-           for (n = 0; n < NSIG; n++) {
-               if (!recvsig[n])
-                   continue;
-               recvsig[n] = FALSE;
-               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) {
+           /* I/O error, kill child if still alive and finish. */
+           schedule_signal(SIGKILL);
+           forward_signals(sv[0]);
            break;
+       }
 #endif /* _PATH_SUDO_IO_LOGDIR */
     }
 
@@ -386,19 +427,211 @@ sudo_execve(path, argv, envp, uid, cstat, dowait, bgmode)
     }
 #endif
 
+done:
     efree(fdsr);
     efree(fdsw);
+#ifdef _PATH_SUDO_IO_LOGDIR
+    while (!tq_empty(&sigfwd_list)) {
+       struct sigforward *sigfwd = tq_first(&sigfwd_list);
+       tq_remove(&sigfwd_list, sigfwd);
+       efree(sigfwd);
+    }
+#endif /* _PATH_SUDO_IO_LOGDIR */
 
     return cstat->type == CMD_ERRNO ? -1 : 0;
 }
 
+/*
+ * Read signals on fd written to by handler().
+ * Returns -1 on error, 0 on child exit, else 1.
+ */
+static int
+handle_signals(fd, child, cstat)
+    int fd;
+    pid_t child;
+    struct command_status *cstat;
+{
+    unsigned char signo;
+    ssize_t nread;
+    int status;
+    pid_t pid;
+
+    for (;;) {
+       /* 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;
+           /* Restart if interrupted by signal so the pipe doesn't fill. */
+           if (errno == EINTR)
+               continue;
+           /* If pipe is empty, we are done. */
+           if (errno == EAGAIN)
+               break;
+           cstat->type = CMD_ERRNO;
+           cstat->val = errno;
+           return -1;
+       }
+       if (signo == SIGCHLD) {
+           /*
+            * If logging I/O, child is the intermediate process,
+            * otherwise it is the command itself.
+            */
+           do {
+#ifdef sudo_waitpid
+               pid = sudo_waitpid(child, &status, WUNTRACED|WNOHANG);
+#else
+               pid = wait(&status);
+#endif
+           } while (pid == -1 && errno == EINTR);
+           if (pid == child) {
+               /* If not logging I/O and child has exited we are done. */
+#ifdef _PATH_SUDO_IO_LOGDIR
+               if (!log_io)
+#endif
+               {
+                   if (WIFSTOPPED(status)) {
+                       /*
+                        * Save the controlling terminal's process group
+                        * so we can restore it after we resume.
+                        */
+#ifdef HAVE_TCSETPGRP
+                       pid_t saved_pgrp = (pid_t)-1;
+                       int fd = open(_PATH_TTY, O_RDWR|O_NOCTTY, 0);
+                       if (fd != -1)
+                           saved_pgrp = tcgetpgrp(fd);
+#endif /* HAVE_TCSETPGRP */
+                       if (kill(getpid(), WSTOPSIG(status)) != 0)
+                           warning("kill(%d, %d)", getpid(), WSTOPSIG(status));
+#ifdef HAVE_TCSETPGRP
+                       if (fd != -1) {
+                           if (saved_pgrp != (pid_t)-1)
+                               (void)tcsetpgrp(fd, saved_pgrp);
+                           close(fd);
+                       }
+#endif /* HAVE_TCSETPGRP */
+                   } 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 {
+#ifdef _PATH_SUDO_IO_LOGDIR
+           if (log_io) {
+               /* Schedule signo to be forwared to the child. */
+               schedule_signal(signo);
+           } else
+#endif
+           {
+               /* Nothing listening on sv[0], send directly. */
+               if (kill(child, signo) != 0)
+                   warning("kill(%d, %d)", child, signo);
+           }
+       }
+    }
+    return 1;
+}
+
+#ifdef _PATH_SUDO_IO_LOGDIR
+/*
+ * Forward signals in sigfwd_list to child listening on fd.
+ */
+static void
+forward_signals(sock)
+    int sock;
+{
+    struct sigforward *sigfwd;
+    struct command_status cstat;
+    ssize_t nsent;
+
+    while (!tq_empty(&sigfwd_list)) {
+       sigfwd = tq_first(&sigfwd_list);
+       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(signo)
+    int signo;
+{
+    struct sigforward *sigfwd;
+
+    sigfwd = emalloc(sizeof(*sigfwd));
+    sigfwd->prev = sigfwd;
+    sigfwd->next = NULL;
+    sigfwd->signo = signo;
+    tq_append(&sigfwd_list, sigfwd);
+}
+#endif /* _PATH_SUDO_IO_LOGDIR */
+
 /*
  * 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
+RETSIGTYPE
 handler(s)
     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.
+     */
+    if (write(signal_pipe[1], &signo, sizeof(signo)) == -1)
+       /* shut up glibc */;
+}
+
+/*
+ * Open a pipe and make both ends non-blocking.
+ * Returns 0 on success and -1 on error.
+ */
+int
+pipe_nonblock(fds)
+    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;
 }