]> granicus.if.org Git - sudo/commitdiff
Reorganize the command execution code to separate out the pty and
authorTodd C. Miller <Todd.Miller@courtesan.com>
Fri, 3 Mar 2017 17:35:11 +0000 (10:35 -0700)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Fri, 3 Mar 2017 17:35:11 +0000 (10:35 -0700)
non-pty code paths into their own event loops.  The non-pty exec
code is now contained in exec_nopty.c and the pty exec code is split
between exec_pty.c (parent process) and exec_monitor.c (session leader).
This results in a small bit of duplicated code but improves readability.
Some of the duplicated code will fall out in future changes to the
event subsystem (the signal pipe).

MANIFEST
src/Makefile.in
src/exec.c
src/exec_monitor.c [new file with mode: 0644]
src/exec_nopty.c [new file with mode: 0644]
src/exec_pty.c
src/sudo_exec.h

index ec7363afbf9cae1494f24c5cff8aa11137379f2e..6d487c73ac4d35edc44913261528d7cfd476d2a5 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -594,6 +594,8 @@ src/conversation.c
 src/env_hooks.c
 src/exec.c
 src/exec_common.c
+src/exec_monitor.c
+src/exec_nopty.c
 src/exec_pty.c
 src/get_pty.c
 src/hooks.c
index 16449a8b47bcc05172be276249395273465b0ac9..b7f13d8c72e462b5f5f0495a7e3d84a84172cdb0 100644 (file)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2010-2015 Todd C. Miller <Todd.Miller@courtesan.com>
+# Copyright (c) 2010-2017 Todd C. Miller <Todd.Miller@courtesan.com>
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -114,10 +114,10 @@ SHELL = @SHELL@
 
 PROGS = @PROGS@
 
-OBJS = conversation.o env_hooks.o exec.o exec_common.o exec_pty.o \
-       get_pty.o hooks.o net_ifs.o load_plugins.o parse_args.o \
-       preserve_fds.o signal.o sudo.o sudo_edit.o tgetpass.o ttyname.o \
-       utmp.o @SUDO_OBJS@
+OBJS = conversation.o env_hooks.o exec.o exec_common.o exec_monitor.o \
+       exec_nopty.o exec_pty.o get_pty.o hooks.o net_ifs.o load_plugins.o \
+       parse_args.o preserve_fds.o signal.o sudo.o sudo_edit.o tgetpass.o \
+       ttyname.o utmp.o @SUDO_OBJS@
 
 SESH_OBJS = sesh.o exec_common.o
 
@@ -299,6 +299,24 @@ exec_common.o: $(srcdir)/exec_common.c $(incdir)/compat/stdbool.h \
                $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
                $(top_builddir)/config.h $(top_builddir)/pathnames.h
        $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_common.c
+exec_monitor.o: $(srcdir)/exec_monitor.c $(incdir)/compat/stdbool.h \
+                $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+                $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+                $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+                $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+                $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
+                $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \
+                $(top_builddir)/pathnames.h
+       $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_monitor.c
+exec_nopty.o: $(srcdir)/exec_nopty.c $(incdir)/compat/stdbool.h \
+              $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+              $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+              $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+              $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+              $(incdir)/sudo_util.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
+              $(srcdir)/sudo_plugin_int.h $(top_builddir)/config.h \
+              $(top_builddir)/pathnames.h
+       $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/exec_nopty.c
 exec_pty.o: $(srcdir)/exec_pty.c $(incdir)/compat/stdbool.h \
             $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
             $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
index 9d24da5a66cd2ea7f97f6d08a20fa1f57e7fd48f..3bc02315873d7557e05d82a4cb87762283b26634 100644 (file)
 #include <config.h>
 
 #include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-#include <sys/wait.h>
-#include <sys/ioctl.h>
 #include <stdio.h>
 #include <stdlib.h>
 #ifdef HAVE_STRING_H
 # include <strings.h>
 #endif /* HAVE_STRINGS_H */
 #include <unistd.h>
-#ifdef TIME_WITH_SYS_TIME
-# include <time.h>
-#endif
 #include <errno.h>
 #include <fcntl.h>
 #include <signal.h>
-#include <termios.h>
 
 #include "sudo.h"
 #include "sudo_exec.h"
 #include "sudo_plugin.h"
 #include "sudo_plugin_int.h"
 
-struct exec_closure {
-    pid_t child;
-    bool log_io;
-    sigset_t omask;
-    struct command_status *cstat;
-    struct command_details *details;
-    struct sudo_event_base *evbase;
-};
-
-/* We keep a tailq of signals to forward to child. */
-struct sigforward {
-    TAILQ_ENTRY(sigforward) entries;
-    int signo;
-};
-TAILQ_HEAD(sigfwd_list, sigforward);
-static struct sigfwd_list sigfwd_list = TAILQ_HEAD_INITIALIZER(sigfwd_list);
-static struct sudo_event *signal_event;
-static struct sudo_event *sigfwd_event;
-static struct sudo_event *backchannel_event;
-static pid_t ppgrp = -1;
-
 volatile pid_t cmnd_pid = -1;
-
-static void signal_pipe_cb(int fd, int what, void *v);
-static int dispatch_pending_signals(struct command_status *cstat);
-static void forward_signals(int fd, int what, void *v);
-static void schedule_signal(struct sudo_event_base *evbase, int signo);
-#ifdef SA_SIGINFO
-static void handler_user_only(int s, siginfo_t *info, void *context);
-#endif
+volatile pid_t ppgrp = -1;
 
 /*
- * Fork and execute a command, returns the child's pid.
- * Sends errno back on sv[1] if execve() fails.
+ * Generic handler for signals received by the sudo front end while the
+ * command is running.  The other end is checked in the main event loop.
  */
-static int
-fork_cmnd(struct command_details *details, int sv[2])
+#ifdef SA_SIGINFO
+void
+exec_handler(int s, siginfo_t *info, void *context)
 {
-    struct command_status cstat;
-    sigaction_t sa;
-    debug_decl(fork_cmnd, SUDO_DEBUG_EXEC)
+    unsigned char signo = (unsigned char)s;
 
-    ppgrp = getpgrp(); /* parent's process group */
+    /*
+     * Do not forward signals sent by a process in the command's process
+     * group, do not forward it as we don't want the child to indirectly
+     * kill itself.  For example, this can happen with some versions of
+     * reboot that call kill(-1, SIGTERM) to kill all other processes.
+     */
+    if (s != SIGCHLD && USER_SIGNALED(info) && info->si_pid != 0) {
+       pid_t si_pgrp = getpgid(info->si_pid);
+       if (si_pgrp != -1) {
+           if (si_pgrp == ppgrp || si_pgrp == cmnd_pid)
+               return;
+       } else if (info->si_pid == cmnd_pid) {
+               return;
+       }
+    }
 
     /*
-     * Handle suspend/restore of sudo and the command.
-     * In most cases, the command will be in the same process group as
-     * sudo and job control will "just work".  However, if the command
-     * changes its process group ID and does not change it back (or is
-     * kill by SIGSTOP which is not catchable), we need to resume the
-     * command manually.  Also, if SIGTSTP is sent directly to sudo,
-     * we need to suspend the command, and then suspend ourself, restoring
-     * the default SIGTSTP handler temporarily.
-     *
-     * XXX - currently we send SIGCONT upon resume in some cases where
-     * we don't need to (e.g. command pgrp == parent pgrp).
+     * The pipe is non-blocking, if we overflow the kernel's pipe
+     * buffer we drop the signal.  This is not a problem in practice.
      */
-    memset(&sa, 0, sizeof(sa));
-    sigfillset(&sa.sa_mask);
-    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
-#ifdef SA_SIGINFO
-    sa.sa_flags |= SA_SIGINFO;
-    sa.sa_sigaction = handler;
+    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
+       if (errno != EINTR)
+           break;
+    }
+}
 #else
-    sa.sa_handler = handler;
-#endif
-    if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
-    if (sudo_sigaction(SIGCONT, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGCONT);
-#ifdef SA_SIGINFO
-    sa.sa_sigaction = handler_user_only;
-#endif
-    if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
+void
+exec_handler(int s)
+{
+    unsigned char signo = (unsigned char)s;
 
-    cmnd_pid = sudo_debug_fork();
-    switch (cmnd_pid) {
-    case -1:
-       sudo_fatal(U_("unable to fork"));
-       break;
-    case 0:
-       /* child */
-       close(sv[0]);
-       close(signal_pipe[0]);
-       close(signal_pipe[1]);
-       (void)fcntl(sv[1], F_SETFD, FD_CLOEXEC);
-       exec_cmnd(details, &cstat, sv[1]);
-       ignore_result(send(sv[1], &cstat, sizeof(cstat), 0));
-       sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
-       _exit(1);
+    /*
+     * The pipe is non-blocking, if we overflow the kernel's pipe
+     * buffer we drop the signal.  This is not a problem in practice.
+     */
+    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
+       if (errno != EINTR)
+           break;
     }
-    sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command,
-       (int)cmnd_pid);
-    debug_return_int(cmnd_pid);
 }
+#endif
 
 /*
  * Setup the execution environment and execute the command.
@@ -149,8 +98,7 @@ fork_cmnd(struct command_details *details, int sv[2])
  * If the exec fails, cstat is filled in with the value of errno.
  */
 void
-exec_cmnd(struct command_details *details, struct command_status *cstat,
-    int errfd)
+exec_cmnd(struct command_details *details, int errfd)
 {
     debug_decl(exec_cmnd, SUDO_DEBUG_EXEC)
 
@@ -184,573 +132,11 @@ exec_cmnd(struct command_details *details, struct command_status *cstat,
                details->envp, ISSET(details->flags, CD_NOEXEC));
        }
     }
-    cstat->type = CMD_ERRNO;
-    cstat->val = errno;
     sudo_debug_printf(SUDO_DEBUG_ERROR, "unable to exec %s: %s",
        details->command, strerror(errno));
     debug_return;
 }
 
-static void
-backchannel_cb(int fd, int what, void *v)
-{
-    struct exec_closure *ec = v;
-    ssize_t n;
-    debug_decl(backchannel_cb, SUDO_DEBUG_EXEC)
-
-    /* read child status */
-    n = recv(fd, ec->cstat, sizeof(struct command_status), MSG_WAITALL);
-    if (n != sizeof(struct command_status)) {
-       if (n == -1) {
-           switch (errno) {
-           case EINTR:
-               /* got a signal, restart loop to service it. */
-               sudo_ev_loopcontinue(ec->evbase);
-               break;
-           case EAGAIN:
-               /* not ready after all... */
-               break;
-           default:
-               ec->cstat->type = CMD_ERRNO;
-               ec->cstat->val = errno;
-               sudo_debug_printf(SUDO_DEBUG_ERROR,
-                   "failed to read child status: %s", strerror(errno));
-               sudo_ev_loopbreak(ec->evbase);
-               break;
-           }
-       } else {
-           /* Short read or EOF. */
-           sudo_debug_printf(SUDO_DEBUG_ERROR,
-               "failed to read child status: %s", n ? "short read" : "EOF");
-           if (!ec->log_io && n == 0) {
-               /*
-                * If not logging I/O we may get EOF when the command is
-                * executed and the other end of the backchannel is closed.
-                * Just remove the event in this case.
-                */
-               sudo_ev_del(ec->evbase, backchannel_event);
-           } else {
-               /* XXX - need new CMD_ type for monitor errors. */
-               errno = n ? EIO : ECONNRESET;
-               ec->cstat->type = CMD_ERRNO;
-               ec->cstat->val = errno;
-               sudo_ev_loopbreak(ec->evbase);
-           }
-       }
-       debug_return;
-    }
-    switch (ec->cstat->type) {
-    case CMD_PID:
-       /*
-        * Once we know the command's pid we can unblock
-        * signals which were blocked in fork_pty().  This
-        * avoids a race between exec of the command and
-        * receipt of a fatal signal from it.
-        */
-       cmnd_pid = ec->cstat->val;
-       sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d",
-           ec->details->command, (int)cmnd_pid);
-       if (ec->log_io)
-           sigprocmask(SIG_SETMASK, &ec->omask, NULL);
-       break;
-    case CMD_WSTATUS:
-       if (WIFSTOPPED(ec->cstat->val)) {
-           /* Suspend parent and tell child how to resume on return. */
-           sudo_debug_printf(SUDO_DEBUG_INFO,
-               "child stopped, suspending parent");
-           n = suspend_parent(WSTOPSIG(ec->cstat->val));
-           schedule_signal(ec->evbase, n);
-           /* Re-enable I/O events and restart event loop to service signal. */
-           add_io_events(ec->evbase);
-           sudo_ev_loopcontinue(ec->evbase);
-       } else {
-           /* Child exited or was killed, either way we are done. */
-           sudo_debug_printf(SUDO_DEBUG_INFO, "child exited or was killed");
-           sudo_ev_loopexit(ec->evbase);
-       }
-       break;
-    case CMD_ERRNO:
-       /* Child was unable to execute command or broken pipe. */
-       sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s",
-           strerror(ec->cstat->val));
-       sudo_ev_loopbreak(ec->evbase);
-       break;
-    }
-    debug_return;
-}
-
-/*
- * Setup initial exec events.
- * Allocates events for the signal pipe and backchannel.
- * Forwarded signals on the backchannel are enabled on demand.
- */
-static struct sudo_event_base *
-exec_event_setup(int backchannel, struct exec_closure *ec)
-{
-    struct sudo_event_base *evbase;
-    debug_decl(exec_event_setup, SUDO_DEBUG_EXEC)
-
-    evbase = sudo_ev_base_alloc();
-    if (evbase == NULL)
-       sudo_fatal(NULL);
-
-    /* Event for incoming signals via signal_pipe. */
-    signal_event = sudo_ev_alloc(signal_pipe[0],
-       SUDO_EV_READ|SUDO_EV_PERSIST, signal_pipe_cb, ec);
-    if (signal_event == NULL)
-       sudo_fatal(NULL);
-    if (sudo_ev_add(evbase, signal_event, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
-
-    /* Event for command status via backchannel. */
-    backchannel_event = sudo_ev_alloc(backchannel,
-       SUDO_EV_READ|SUDO_EV_PERSIST, backchannel_cb, ec);
-    if (backchannel_event == NULL)
-       sudo_fatal(NULL);
-    if (sudo_ev_add(evbase, backchannel_event, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
-
-    /* The signal forwarding event gets added on demand. */
-    sigfwd_event = sudo_ev_alloc(backchannel,
-       SUDO_EV_WRITE, forward_signals, NULL);
-    if (sigfwd_event == NULL)
-       sudo_fatal(NULL);
-
-    sudo_debug_printf(SUDO_DEBUG_INFO, "signal pipe fd %d\n", signal_pipe[0]);
-    sudo_debug_printf(SUDO_DEBUG_INFO, "backchannel fd %d\n", backchannel);
-
-    debug_return_ptr(evbase);
-}
-
-/*
- * Execute a command, potentially in a pty with I/O loggging, and
- * wait for it to finish.
- * This is a little bit tricky due to how POSIX job control works and
- * we fact that we have two different controlling terminals to deal with.
- */
-int
-sudo_execute(struct command_details *details, struct command_status *cstat)
-{
-    struct sigforward *sigfwd, *sigfwd_next;
-    const char *utmp_user = NULL;
-    struct sudo_event_base *evbase;
-    struct exec_closure ec;
-    bool log_io = false;
-    sigaction_t sa;
-    pid_t child;
-    int sv[2];
-    debug_decl(sudo_execute, SUDO_DEBUG_EXEC)
-
-    dispatch_pending_signals(cstat);
-
-    /* If running in background mode, fork and exit. */
-    if (ISSET(details->flags, CD_BACKGROUND)) {
-       switch (sudo_debug_fork()) {
-           case -1:
-               cstat->type = CMD_ERRNO;
-               cstat->val = errno;
-               debug_return_int(-1);
-           case 0:
-               /* child continues without controlling terminal */
-               (void)setpgid(0, 0);
-               break;
-           default:
-               /* parent exits (but does not flush buffers) */
-               sudo_debug_exit_int(__func__, __FILE__, __LINE__,
-                   sudo_debug_subsys, 0);
-               _exit(0);
-       }
-    }
-
-    /*
-     * If we have an I/O plugin or the policy plugin has requested one, we
-     * need to allocate a pty.  It is OK to set log_io in the pty-only case
-     * as the io plugin tailqueue will be empty and no I/O logging will occur.
-     */
-    if (!TAILQ_EMPTY(&io_plugins) || ISSET(details->flags, CD_USE_PTY)) {
-       log_io = true;
-       if (ISSET(details->flags, CD_SET_UTMP))
-           utmp_user = details->utmp_user ? details->utmp_user : user_details.username;
-       sudo_debug_printf(SUDO_DEBUG_INFO, "allocate pty for I/O logging");
-       pty_setup(details->euid, user_details.tty, utmp_user);
-    } else if (!ISSET(details->flags, CD_SET_TIMEOUT|CD_SUDOEDIT) &&
-       policy_plugin.u.policy->close == NULL) {
-       /*
-        * If there is no policy close function, no I/O logging or pty,
-        * and we were not invoked as sudoedit, just exec directly.
-        */
-       exec_cmnd(details, cstat, -1);
-       goto done;
-    }
-
-    /*
-     * 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_STREAM, 0, sv) == -1)
-       sudo_fatal(U_("unable to create sockets"));
-
-    /*
-     * Signals to forward to the child process (excluding SIGALRM).
-     * We block all other signals while running the signal handler.
-     * Note: HP-UX select() will not be interrupted if SA_RESTART set.
-     */
-    memset(&sa, 0, sizeof(sa));
-    sigfillset(&sa.sa_mask);
-    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
-#ifdef SA_SIGINFO
-    sa.sa_flags |= SA_SIGINFO;
-    sa.sa_sigaction = handler;
-#else
-    sa.sa_handler = handler;
-#endif
-    if (sudo_sigaction(SIGTERM, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTERM);
-    if (sudo_sigaction(SIGHUP, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGHUP);
-    if (sudo_sigaction(SIGALRM, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGALRM);
-    if (sudo_sigaction(SIGPIPE, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGPIPE);
-    if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR1);
-    if (sudo_sigaction(SIGUSR2, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR2);
-#ifdef SIGINFO
-    if (sudo_sigaction(SIGINFO, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGINFO);
-#endif
-
-    /*
-     * When not running the command in a pty, we do not want to
-     * forward signals generated by the kernel that the child will
-     * already have received either by virtue of being in the
-     * controlling tty's process group (SIGINT, SIGQUIT).
-     */
-#ifdef SA_SIGINFO
-    if (!log_io) {
-       sa.sa_flags |= SA_SIGINFO;
-       sa.sa_sigaction = handler_user_only;
-    }
-#endif
-    if (sudo_sigaction(SIGINT, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGINT);
-    if (sudo_sigaction(SIGQUIT, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGQUIT);
-
-    /*
-     * The policy plugin's session init must be run before we fork
-     * or certain pam modules won't be able to track their state.
-     */
-    if (policy_init_session(details) != true)
-       sudo_fatalx(U_("policy plugin failed session initialization"));
-
-    /*
-     * Child will run the command in the pty, parent will pass data
-     * to and from pty.
-     */
-    if (log_io)
-       child = fork_pty(details, sv, &ec.omask);
-    else
-       child = fork_cmnd(details, sv);
-    close(sv[1]);
-
-    /* No longer need execfd. */
-    if (details->execfd != -1) {
-       close(details->execfd);
-       details->execfd = -1;
-    }
-
-    /* Set command timeout if specified. */
-    if (ISSET(details->flags, CD_SET_TIMEOUT))
-       alarm(details->timeout);
-
-    /*
-     * I/O logging must be in the C locale for floating point numbers
-     * to be logged consistently.
-     */
-    setlocale(LC_ALL, "C");
-
-    /*
-     * Allocate event base and two persistent events:
-     * the signal pipe and the child process's backchannel.
-     */
-    evbase = exec_event_setup(sv[0], &ec);
-
-    /*
-     * Generic exec closure used for signal_pipe and backchannel callbacks.
-     * Note ec.omask is set earlier.
-     */
-    ec.child = child;
-    ec.log_io = log_io;
-    ec.cstat = cstat;
-    ec.evbase = evbase;
-    ec.details = details;
-
-    /*
-     * In the event loop we pass input from user tty to master
-     * and pass output from master to stdout and IO plugin.
-     */
-    if (log_io)
-       add_io_events(evbase);
-    if (sudo_ev_loop(evbase, 0) == -1)
-       sudo_warn(U_("error in event loop"));
-    if (sudo_ev_got_break(evbase)) {
-       /* error from callback */
-       sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely");
-       /* kill command if not I/O logging */
-       if (!log_io)
-           terminate_command(ec.child, true);
-    }
-
-    if (log_io) {
-       /* Flush any remaining output and free pty-related memory. */
-       pty_close(cstat);
-   }
-
-#ifdef HAVE_SELINUX
-    if (ISSET(details->flags, CD_RBAC_ENABLED)) {
-       /* This is probably not needed in log_io mode. */
-       if (selinux_restore_tty() != 0)
-           sudo_warnx(U_("unable to restore tty label"));
-    }
-#endif
-
-    /* Free things up. */
-    sudo_ev_base_free(evbase);
-    sudo_ev_free(sigfwd_event);
-    sudo_ev_free(signal_event);
-    sudo_ev_free(backchannel_event);
-    TAILQ_FOREACH_SAFE(sigfwd, &sigfwd_list, entries, sigfwd_next) {
-       free(sigfwd);
-    }
-    TAILQ_INIT(&sigfwd_list);
-done:
-    debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0);
-}
-
-/*
- * Forward a signal to the command (non-pty version) or handle
- * changes to the command's status (SIGCHLD).
- */
-static void
-dispatch_signal(struct exec_closure *ec, int signo, char *signame)
-{
-    debug_decl(dispatch_signal, SUDO_DEBUG_EXEC)
-
-    sudo_debug_printf(SUDO_DEBUG_INFO,
-       "%s: evbase %p, child: %d, signo %s(%d), cstat %p",
-       __func__, ec->evbase, (int)ec->child, signame, signo, ec->cstat);
-
-    if (ec->child == -1)
-       goto done;
-
-    if (signo == SIGCHLD) {
-       pid_t pid;
-       int status;
-       /*
-        * The command stopped or exited.
-        */
-       do {
-           pid = waitpid(ec->child, &status, WUNTRACED|WNOHANG);
-       } while (pid == -1 && errno == EINTR);
-       if (pid == ec->child) {
-           if (WIFSTOPPED(status)) {
-               /*
-                * Save the controlling terminal's process group
-                * so we can restore it after we resume, if needed.
-                * Most well-behaved shells change the pgrp back to
-                * its original value before suspending so we must
-                * not try to restore in that case, lest we race with
-                * the child upon resume, potentially stopping sudo
-                * with SIGTTOU while the command continues to run.
-                */
-               sigaction_t sa, osa;
-               pid_t saved_pgrp = -1;
-               int signo = WSTOPSIG(status);
-               int fd = open(_PATH_TTY, O_RDWR);
-               if (fd != -1) {
-                   saved_pgrp = tcgetpgrp(fd);
-                   if (saved_pgrp == -1) {
-                       close(fd);
-                       fd = -1;
-                   }
-               }
-               if (saved_pgrp != -1) {
-                   /*
-                    * Child was stopped trying to access controlling
-                    * terminal.  If the child has a different pgrp
-                    * and we own the controlling terminal, give it
-                    * to the child's pgrp and let it continue.
-                    */
-                   if (signo == SIGTTOU || signo == SIGTTIN) {
-                       if (saved_pgrp == ppgrp) {
-                           pid_t child_pgrp = getpgid(ec->child);
-                           if (child_pgrp != ppgrp) {
-                               if (tcsetpgrp(fd, child_pgrp) == 0) {
-                                   if (killpg(child_pgrp, SIGCONT) != 0) {
-                                       sudo_warn("kill(%d, SIGCONT)",
-                                           (int)child_pgrp);
-                                   }
-                                   close(fd);
-                                   goto done;
-                               }
-                           }
-                       }
-                   }
-               }
-               if (signo == SIGTSTP) {
-                   memset(&sa, 0, sizeof(sa));
-                   sigemptyset(&sa.sa_mask);
-                   sa.sa_flags = SA_RESTART;
-                   sa.sa_handler = SIG_DFL;
-                   if (sudo_sigaction(SIGTSTP, &sa, &osa) != 0) {
-                       sudo_warn(U_("unable to set handler for signal %d"),
-                           SIGTSTP);
-                   }
-               }
-               if (kill(getpid(), signo) != 0)
-                   sudo_warn("kill(%d, SIG%s)", (int)getpid(), signame);
-               if (signo == SIGTSTP) {
-                   if (sudo_sigaction(SIGTSTP, &osa, NULL) != 0) {
-                       sudo_warn(U_("unable to restore handler for signal %d"),
-                           SIGTSTP);
-                   }
-               }
-               if (saved_pgrp != -1) {
-                   /*
-                    * Restore command's process group if different.
-                    * Otherwise, we cannot resume some shells.
-                    */
-                   if (saved_pgrp != ppgrp)
-                       (void)tcsetpgrp(fd, saved_pgrp);
-                   close(fd);
-               }
-           } else {
-               /* Child has exited or been killed, we are done. */
-               ec->child = -1;
-               ec->cstat->type = CMD_WSTATUS;
-               ec->cstat->val = status;
-               sudo_ev_del(ec->evbase, signal_event);
-               sudo_ev_loopexit(ec->evbase);
-               goto done;
-           }
-       }
-    } else {
-       /* Send signal to child. */
-       if (signo == SIGALRM) {
-           terminate_command(ec->child, false);
-       } else if (kill(ec->child, signo) != 0) {
-           sudo_warn("kill(%d, SIG%s)", (int)ec->child, signame);
-       }
-    }
-done:
-    debug_return;
-}
-
-/*
- * Forward a signal to the monitor (pty version) or handle
- * changes to the monitors's status (SIGCHLD).
- */
-static void
-dispatch_signal_pty(struct exec_closure *ec, int signo, char *signame)
-{
-    debug_decl(dispatch_signal_pty, SUDO_DEBUG_EXEC)
-
-    sudo_debug_printf(SUDO_DEBUG_INFO,
-       "%s: evbase %p, child: %d, signo %s(%d), cstat %p",
-       __func__, ec->evbase, (int)ec->child, signame, signo, ec->cstat);
-
-    if (ec->child == -1)
-       goto done;
-
-    if (signo == SIGCHLD) {
-       int n, status;
-       pid_t pid;
-       /*
-        * Monitor process was signaled; wait for it as needed.
-        */
-       do {
-           pid = waitpid(ec->child, &status, WUNTRACED|WNOHANG);
-       } while (pid == -1 && errno == EINTR);
-       if (pid == ec->child) {
-           /*
-            * If the monitor dies we get notified via backchannel_cb().
-            * If it was stopped, we should stop too (the command keeps
-            * running in its pty) and continue it when we come back.
-            */
-           if (WIFSTOPPED(status)) {
-               sudo_debug_printf(SUDO_DEBUG_INFO,
-                   "monitor stopped, suspending parent");
-               n = suspend_parent(WSTOPSIG(status));
-               kill(pid, SIGCONT);
-               schedule_signal(ec->evbase, n);
-               /* Re-enable I/O events and restart event loop. */
-               add_io_events(ec->evbase);
-               sudo_ev_loopcontinue(ec->evbase);
-               goto done;
-           } else if (WIFSIGNALED(status)) {
-               sudo_debug_printf(SUDO_DEBUG_INFO,
-                   "monitor killed, signal %d", WTERMSIG(status));
-               ec->child = -1;
-           } else {
-               sudo_debug_printf(SUDO_DEBUG_INFO,
-                   "monitor exited, status %d", WEXITSTATUS(status));
-               ec->child = -1;
-           }
-       }
-    } else {
-       /* Schedule signo to be forwared to the child. */
-       schedule_signal(ec->evbase, signo);
-       /* Restart event loop to service signal immediately. */
-       sudo_ev_loopcontinue(ec->evbase);
-    }
-done:
-    debug_return;
-}
-
-/* Signal pipe callback */
-static void
-signal_pipe_cb(int fd, int what, void *v)
-{
-    struct exec_closure *ec = v;
-    char signame[SIG2STR_MAX];
-    unsigned char signo;
-    ssize_t nread;
-    debug_decl(signal_pipe_cb, SUDO_DEBUG_EXEC)
-
-    /* Process received signals until the child dies or the pipe is empty. */
-    do {
-       /* read signal pipe */
-       nread = read(fd, &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;
-           /* On error, store errno and break out of the event loop. */
-           if (errno != EAGAIN) {
-               ec->cstat->type = CMD_ERRNO;
-               ec->cstat->val = errno;
-               sudo_warn(U_("error reading from signal pipe"));
-               sudo_ev_loopbreak(ec->evbase);
-           }
-           break;
-       }
-       if (sig2str(signo, signame) == -1)
-           snprintf(signame, sizeof(signame), "%d", signo);
-       sudo_debug_printf(SUDO_DEBUG_DIAG, "received SIG%s", signame);
-       if (ec->log_io) {
-           dispatch_signal_pty(ec, signo, signame);
-       } else {
-           dispatch_signal(ec, signo, signame);
-       }
-    } while (ec->child != -1);
-    debug_return;
-}
-
 /*
  * Drain pending signals from signal_pipe written by sudo_handler().
  * Handles the case where the signal was sent to us before
@@ -809,173 +195,66 @@ dispatch_pending_signals(struct command_status *cstat)
 }
 
 /*
- * Forward signals in sigfwd_list to child listening on fd.
- */
-static void
-forward_signals(int sock, int what, void *v)
-{
-    char signame[SIG2STR_MAX];
-    struct sigforward *sigfwd;
-    struct command_status cstat;
-    ssize_t nsent;
-    debug_decl(forward_signals, SUDO_DEBUG_EXEC)
-
-    while (!TAILQ_EMPTY(&sigfwd_list)) {
-       sigfwd = TAILQ_FIRST(&sigfwd_list);
-       if (sigfwd->signo == SIGCONT_FG)
-           strlcpy(signame, "CONT_FG", sizeof(signame));
-       else if (sigfwd->signo == SIGCONT_BG)
-           strlcpy(signame, "CONT_BG", sizeof(signame));
-       else if (sig2str(sigfwd->signo, signame) == -1)
-           snprintf(signame, sizeof(signame), "%d", sigfwd->signo);
-       sudo_debug_printf(SUDO_DEBUG_INFO,
-           "sending SIG%s to child over backchannel", signame);
-       cstat.type = CMD_SIGNO;
-       cstat.val = sigfwd->signo;
-       do {
-           nsent = send(sock, &cstat, sizeof(cstat), 0);
-       } while (nsent == -1 && errno == EINTR);
-       TAILQ_REMOVE(&sigfwd_list, sigfwd, entries);
-       free(sigfwd);
-       if (nsent != sizeof(cstat)) {
-           if (errno == EPIPE) {
-               struct sigforward *sigfwd_next;
-               sudo_debug_printf(SUDO_DEBUG_ERROR,
-                   "broken pipe writing to child over backchannel");
-               /* Other end of socket gone, empty out sigfwd_list. */
-               TAILQ_FOREACH_SAFE(sigfwd, &sigfwd_list, entries, sigfwd_next) {
-                   free(sigfwd);
-               }
-               TAILQ_INIT(&sigfwd_list);
-               /* XXX - child (monitor) is dead, we should exit too? */
-           }
-           break;
-       }
-    }
-}
-
-/*
- * Schedule a signal to be forwarded.
- */
-static void
-schedule_signal(struct sudo_event_base *evbase, int signo)
-{
-    struct sigforward *sigfwd;
-    char signame[SIG2STR_MAX];
-    debug_decl(schedule_signal, SUDO_DEBUG_EXEC)
-
-    if (signo == SIGCONT_FG)
-       strlcpy(signame, "CONT_FG", sizeof(signame));
-    else if (signo == SIGCONT_BG)
-       strlcpy(signame, "CONT_BG", sizeof(signame));
-    else if (sig2str(signo, signame) == -1)
-       snprintf(signame, sizeof(signame), "%d", signo);
-    sudo_debug_printf(SUDO_DEBUG_DIAG, "scheduled SIG%s for child", signame);
-
-    if ((sigfwd = calloc(1, sizeof(*sigfwd))) == NULL)
-       sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
-    sigfwd->signo = signo;
-    TAILQ_INSERT_TAIL(&sigfwd_list, sigfwd, entries);
-
-    if (sudo_ev_add(evbase, sigfwd_event, NULL, true) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
-
-    debug_return;
-}
-
-/*
- * Generic handler for signals passed from parent -> child.
- * The other end of signal_pipe is checked in the main event loop.
+ * Execute a command, potentially in a pty with I/O loggging, and
+ * wait for it to finish.
+ * This is a little bit tricky due to how POSIX job control works and
+ * we fact that we have two different controlling terminals to deal with.
  */
-#ifdef SA_SIGINFO
-void
-handler(int s, siginfo_t *info, void *context)
-{
-    unsigned char signo = (unsigned char)s;
-
-    /*
-     * Do not forward signals sent by a process in the command's process
-     * group, do not forward it as we don't want the child to indirectly
-     * kill itself.  For example, this can happen with some versions of
-     * reboot that call kill(-1, SIGTERM) to kill all other processes.
-     */
-    if (s != SIGCHLD && USER_SIGNALED(info) && info->si_pid != 0) {
-       pid_t si_pgrp = getpgid(info->si_pid);
-       if (si_pgrp != -1) {
-           if (si_pgrp == ppgrp || si_pgrp == cmnd_pid)
-               return;
-       } else if (info->si_pid == cmnd_pid) {
-               return;
-       }
-    }
-
-    /*
-     * The pipe is non-blocking, if we overflow the kernel's pipe
-     * buffer we drop the signal.  This is not a problem in practice.
-     */
-    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
-       if (errno != EINTR)
-           break;
-    }
-}
-#else
-void
-handler(int s)
+int
+sudo_execute(struct command_details *details, struct command_status *cstat)
 {
-    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.
-     */
-    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
-       if (errno != EINTR)
-           break;
-    }
-}
-#endif
+    debug_decl(sudo_execute, SUDO_DEBUG_EXEC)
 
-#ifdef SA_SIGINFO
-/*
- * Generic handler for signals passed from parent -> child.
- * The other end of signal_pipe is checked in the main event loop.
- * This version is for the non-pty case and does not forward
- * signals that are generated by the kernel.
- */
-static void
-handler_user_only(int s, siginfo_t *info, void *context)
-{
-    unsigned char signo = (unsigned char)s;
+    dispatch_pending_signals(cstat);
 
-    /*
-     * Only forward user-generated signals not sent by a process in
-     * the command's own process group.  Signals sent by the kernel
-     * may include SIGTSTP when the user presses ^Z.  Curses programs
-     * often trap ^Z and send SIGTSTP to their own pgrp, so we don't
-     * want to send an extra SIGTSTP.
-     */
-    if (!USER_SIGNALED(info))
-       return;
-    if (info->si_pid != 0) {
-       pid_t si_pgrp = getpgid(info->si_pid);
-       if (si_pgrp != -1) {
-           if (si_pgrp == ppgrp || si_pgrp == cmnd_pid)
-               return;
-       } else if (info->si_pid == cmnd_pid) {
-           return;
+    /* If running in background mode, fork and exit. */
+    if (ISSET(details->flags, CD_BACKGROUND)) {
+       switch (sudo_debug_fork()) {
+           case -1:
+               cstat->type = CMD_ERRNO;
+               cstat->val = errno;
+               debug_return_int(-1);
+           case 0:
+               /* child continues without controlling terminal */
+               (void)setpgid(0, 0);
+               break;
+           default:
+               /* parent exits (but does not flush buffers) */
+               sudo_debug_exit_int(__func__, __FILE__, __LINE__,
+                   sudo_debug_subsys, 0);
+               _exit(0);
        }
     }
 
     /*
-     * 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 we have an I/O plugin or the policy plugin has requested one, we
+     * need to allocate a pty.
      */
-    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
-       if (errno != EINTR)
-           break;
+    if (!TAILQ_EMPTY(&io_plugins) || ISSET(details->flags, CD_USE_PTY)) {
+       /*
+        * Run the command in a new pty, wait for it to finish and
+        * send the plugin the exit status.
+        */
+       exec_pty(details, cstat);
+    } else if (!ISSET(details->flags, CD_SET_TIMEOUT|CD_SUDOEDIT) &&
+       policy_plugin.u.policy->close == NULL) {
+       /*
+        * If we are not running the command in a pty, we were not invoked
+        * as sudoedit, there is no command timeout and there is no close
+        * function, just exec directly.  Only returns on error.
+        */
+       exec_cmnd(details, -1);
+       cstat->type = CMD_ERRNO;
+       cstat->val = errno;
+    } else {
+       /*
+        * No pty but we need to wait for the command to finish to
+        * send the plugin the exit status.
+        */
+       exec_nopty(details, cstat);
     }
+    debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0);
 }
-#endif /* SA_SIGINFO */
 
 /*
  * Open a pipe and make both ends non-blocking.
@@ -1005,3 +284,39 @@ pipe_nonblock(int fds[2])
 
     debug_return_int(ret);
 }
+
+/*
+ * Kill command with increasing urgency.
+ */
+void
+terminate_command(pid_t pid, bool use_pgrp)
+{
+    debug_decl(terminate_command, SUDO_DEBUG_EXEC);
+
+    /* Avoid killing more than a single process or process group. */
+    if (pid <= 0)
+       debug_return;
+
+    /*
+     * Note that SIGCHLD will interrupt the sleep()
+     */
+    if (use_pgrp) {
+       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGHUP", (int)pid);
+       killpg(pid, SIGHUP);
+       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGTERM", (int)pid);
+       killpg(pid, SIGTERM);
+       sleep(2);
+       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGKILL", (int)pid);
+       killpg(pid, SIGKILL);
+    } else {
+       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGHUP", (int)pid);
+       kill(pid, SIGHUP);
+       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGTERM", (int)pid);
+       kill(pid, SIGTERM);
+       sleep(2);
+       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGKILL", (int)pid);
+       kill(pid, SIGKILL);
+    }
+
+    debug_return;
+}
diff --git a/src/exec_monitor.c b/src/exec_monitor.c
new file mode 100644 (file)
index 0000000..b447f47
--- /dev/null
@@ -0,0 +1,664 @@
+/*
+ * Copyright (c) 2009-2017 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif /* HAVE_STRINGS_H */
+#include <unistd.h>
+#include <time.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#include "sudo.h"
+#include "sudo_event.h"
+#include "sudo_exec.h"
+#include "sudo_plugin.h"
+#include "sudo_plugin_int.h"
+
+struct monitor_closure {
+    struct sudo_event_base *evbase;
+    struct sudo_event *errpipe_event;
+    struct sudo_event *backchannel_event;
+    struct sudo_event *signal_pipe_event;
+    struct command_status *cstat;
+    int backchannel;
+};
+
+static volatile pid_t cmnd_pgrp;
+static pid_t mon_pgrp;
+
+extern int io_fds[6]; /* XXX */
+
+/*
+ * Generic handler for signals recieved by the monitor process.
+ * The other end of signal_pipe is checked in the monitor event loop.
+ */
+#ifdef SA_SIGINFO
+static void
+mon_handler(int s, siginfo_t *info, void *context)
+{
+    unsigned char signo = (unsigned char)s;
+
+    /*
+     * If the signal came from the process group of the command we ran,
+     * do not forward it as we don't want the child to indirectly kill
+     * itself.  This can happen with, e.g., BSD-derived versions of
+     * reboot that call kill(-1, SIGTERM) to kill all other processes.
+     */
+    if (s != SIGCHLD && USER_SIGNALED(info) && info->si_pid != 0) {
+       pid_t si_pgrp = getpgid(info->si_pid);
+       if (si_pgrp != -1) {
+           if (si_pgrp == cmnd_pgrp)
+               return;
+       } else if (info->si_pid == cmnd_pid) {
+               return;
+       }
+    }
+
+    /*
+     * The pipe is non-blocking, if we overflow the kernel's pipe
+     * buffer we drop the signal.  This is not a problem in practice.
+     */
+    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
+       if (errno != EINTR)
+           break;
+    }
+}
+#else
+static void
+mon_handler(int s)
+{
+    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.
+     */
+    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
+       if (errno != EINTR)
+           break;
+    }
+}
+#endif
+
+/*
+ * Deliver a signal to the running command.
+ * The signal was either forwarded to us by the parent sudo process
+ * or was received by the monitor itself.
+ *
+ * There are two "special" signals, SIGCONT_FG and SIGCONT_BG that
+ * also specify whether the command should have the controlling tty.
+ */
+static void
+deliver_signal(pid_t pid, int signo, bool from_parent)
+{
+    char signame[SIG2STR_MAX];
+    int status;
+    debug_decl(deliver_signal, SUDO_DEBUG_EXEC);
+
+    /* Avoid killing more than a single process or process group. */
+    if (pid <= 0)
+       debug_return;
+
+    if (signo == SIGCONT_FG)
+       strlcpy(signame, "CONT_FG", sizeof(signame));
+    else if (signo == SIGCONT_BG)
+       strlcpy(signame, "CONT_BG", sizeof(signame));
+    else if (sig2str(signo, signame) == -1)
+       snprintf(signame, sizeof(signame), "%d", signo);
+
+    /* Handle signal from parent or monitor. */
+    sudo_debug_printf(SUDO_DEBUG_INFO, "received SIG%s%s",
+       signame, from_parent ? " from parent" : "");
+    switch (signo) {
+    case SIGALRM:
+       terminate_command(pid, true);
+       break;
+    case SIGCONT_FG:
+       /* Continue in foreground, grant it controlling tty. */
+       do {
+           status = tcsetpgrp(io_fds[SFD_SLAVE], cmnd_pgrp);
+       } while (status == -1 && errno == EINTR);
+       killpg(pid, SIGCONT);
+       break;
+    case SIGCONT_BG:
+       /* Continue in background, I take controlling tty. */
+       do {
+           status = tcsetpgrp(io_fds[SFD_SLAVE], mon_pgrp);
+       } while (status == -1 && errno == EINTR);
+       killpg(pid, SIGCONT);
+       break;
+    case SIGKILL:
+       _exit(1); /* XXX */
+       /* NOTREACHED */
+    default:
+       /* Relay signal to command. */
+       killpg(pid, signo);
+       break;
+    }
+    debug_return;
+}
+
+/*
+ * Send status to parent over socketpair.
+ * Return value is the same as send(2).
+ */
+static int
+send_status(int fd, struct command_status *cstat)
+{
+    int n = -1;
+    debug_decl(send_status, SUDO_DEBUG_EXEC);
+
+    if (cstat->type != CMD_INVALID) {
+       sudo_debug_printf(SUDO_DEBUG_INFO,
+           "sending status message to parent: [%d, %d]",
+           cstat->type, cstat->val);
+       do {
+           n = send(fd, cstat, sizeof(*cstat), 0);
+       } while (n == -1 && errno == EINTR);
+       if (n != sizeof(*cstat)) {
+           sudo_debug_printf(SUDO_DEBUG_ERROR,
+               "unable to send status to parent: %s", strerror(errno));
+       }
+       cstat->type = CMD_INVALID; /* prevent re-sending */
+    }
+    debug_return_int(n);
+}
+
+/*
+ * Wait for command status after receiving SIGCHLD.
+ * If the command was stopped, the status is send back to the parent.
+ * Otherwise, cstat is filled in but not sent.
+ */
+static void
+handle_sigchld(int backchannel, struct command_status *cstat)
+{
+    char signame[SIG2STR_MAX];
+    int status;
+    pid_t pid;
+    debug_decl(handle_sigchld, SUDO_DEBUG_EXEC);
+
+    /* Read command status. */
+    do {
+       pid = waitpid(cmnd_pid, &status, WUNTRACED|WCONTINUED|WNOHANG);
+    } while (pid == -1 && errno == EINTR);
+    switch (pid) {
+    case 0:
+       errno = ECHILD;
+       /* FALLTHROUGH */
+    case -1:
+       sudo_debug_printf(SUDO_DEBUG_DIAG,
+           "waitpid returned %d, expected pid %d", pid, cmnd_pid);
+       sudo_warn(U_("%s: %s"), __func__, "waitpid");
+       debug_return;
+    }
+
+    if (WIFCONTINUED(status)) {
+       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) resumed",
+           __func__, cmnd_pid);
+    } else if (WIFSTOPPED(status)) {
+       if (sig2str(WSTOPSIG(status), signame) == -1)
+           snprintf(signame, sizeof(signame), "%d", WSTOPSIG(status));
+       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s",
+           __func__, cmnd_pid, signame);
+    } else if (WIFSIGNALED(status)) {
+       if (sig2str(WTERMSIG(status), signame) == -1)
+           snprintf(signame, sizeof(signame), "%d", WTERMSIG(status));
+       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) killed, SIG%s",
+           __func__, cmnd_pid, signame);
+       cmnd_pid = -1;
+    } else if (WIFEXITED(status)) {
+       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) exited: %d",
+           __func__, cmnd_pid, WEXITSTATUS(status));
+       cmnd_pid = -1;
+    } else {
+       sudo_debug_printf(SUDO_DEBUG_WARN,
+           "%s: unexpected wait status %d for command (%d)",
+           __func__, status, (int)cmnd_pid);
+    }
+
+    /* Don't overwrite execve() failure with child exit status. */
+    if (cstat->type != CMD_ERRNO) {
+       /*
+        * Store wait status in cstat and forward to parent if stopped.
+        */
+       cstat->type = CMD_WSTATUS;
+       cstat->val = status;
+       if (WIFSTOPPED(status)) {
+           /* Save the foreground pgid so we can restore it later. */
+           do {
+               pid = tcgetpgrp(io_fds[SFD_SLAVE]);
+           } while (pid == -1 && errno == EINTR);
+           if (pid != mon_pgrp)
+               cmnd_pgrp = pid;
+           send_status(backchannel, cstat);
+       }
+    }
+
+    debug_return;
+}
+
+static void
+mon_signal_pipe_cb(int fd, int what, void *v)
+{
+    struct monitor_closure *mc = v;
+    unsigned char signo;
+    ssize_t nread;
+    debug_decl(mon_signal_pipe_cb, SUDO_DEBUG_EXEC);
+
+    nread = read(fd, &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_warn(U_("error reading from signal pipe"));
+           sudo_ev_loopbreak(mc->evbase);
+       }
+    } else {
+       /*
+        * Handle SIGCHLD specially and deliver other signals
+        * directly to the command.
+        */
+       if (signo == SIGCHLD) {
+           handle_sigchld(mc->backchannel, mc->cstat);
+           if (cmnd_pid == -1) {
+               /* Remove all but the errpipe event. */
+               sudo_ev_del(mc->evbase, mc->backchannel_event);
+               sudo_ev_del(mc->evbase, mc->signal_pipe_event);
+           }
+       } else {
+           deliver_signal(cmnd_pid, signo, false);
+       }
+    }
+    debug_return;
+}
+
+/* Note: this is basically the same as errpipe_cb() in exec_nopty.c */
+static void
+mon_errpipe_cb(int fd, int what, void *v)
+{
+    struct monitor_closure *mc = v;
+    ssize_t n;
+    int errval;
+    debug_decl(mon_errpipe_cb, SUDO_DEBUG_EXEC);
+
+    /* read errno from child or EOF when command is executed. */
+    n = read(fd, &errval, sizeof(errval));
+    switch (n) {
+    case -1:
+       switch (errno) {
+       case EINTR:
+           /* got a signal, restart loop to service it. */
+           sudo_ev_loopcontinue(mc->evbase);
+           break;
+       case EAGAIN:
+           /* not ready after all... */
+           break;
+       default:
+           sudo_debug_printf(SUDO_DEBUG_ERROR,
+               "failed to read error pipe: %s", strerror(errno));
+           mc->cstat->type = CMD_ERRNO;
+           mc->cstat->val = errno;
+           sudo_ev_loopbreak(mc->evbase);
+           break;
+       }
+       break;
+    case 0:
+       /*
+        * We get EOF when the command is executed and the other
+        * end of the error pipe is closed.  Just remove the event.
+        */
+       sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error pipe, removing event");
+       sudo_ev_del(mc->evbase, mc->errpipe_event);
+       break;
+    default:
+       /* Errno value when child is unable to execute command. */
+       sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s",
+           strerror(errval));
+       mc->cstat->type = CMD_ERRNO;
+       mc->cstat->val = errval;
+       sudo_ev_del(mc->evbase, mc->errpipe_event);
+       break;
+    }
+    debug_return;
+}
+
+static void
+mon_backchannel_cb(int fd, int what, void *v)
+{
+    struct monitor_closure *mc = v;
+    struct command_status cstmp;
+    ssize_t n;
+    debug_decl(mon_backchannel_cb, SUDO_DEBUG_EXEC);
+
+    /* Read command from backchannel, should be a signal. */
+    n = recv(fd, &cstmp, sizeof(cstmp), MSG_WAITALL);
+    if (n != sizeof(cstmp)) {
+       if (n == -1) {
+           if (errno == EINTR || errno == EAGAIN)
+               debug_return;
+           sudo_warn(U_("error reading from socketpair"));
+       } else {
+           /* short read or EOF, parent process died? */
+       }
+       sudo_ev_loopbreak(mc->evbase);
+    } else {
+       if (cstmp.type == CMD_SIGNO) {
+           deliver_signal(cmnd_pid, cstmp.val, true);
+       } else {
+           sudo_warnx(U_("unexpected reply type on backchannel: %d"), cstmp.type);
+       }
+    }
+    debug_return;
+}
+
+/*
+ * Sets up std{in,out,err} and executes the actual command.
+ * Returns only if execve() fails.
+ */
+static void
+exec_cmnd_pty(struct command_details *details, bool foreground, int errfd)
+{
+    volatile pid_t self = getpid();
+    debug_decl(exec_cmnd_pty, SUDO_DEBUG_EXEC);
+
+    /* Register cleanup function */
+    sudo_fatal_callback_register(pty_cleanup);
+
+    /* Set command process group here too to avoid a race. */
+    setpgid(0, self);
+
+    /* Wire up standard fds, note that stdout/stderr may be pipes. */
+    if (dup2(io_fds[SFD_STDIN], STDIN_FILENO) == -1 ||
+       dup2(io_fds[SFD_STDOUT], STDOUT_FILENO) == -1 ||
+       dup2(io_fds[SFD_STDERR], STDERR_FILENO) == -1)
+       sudo_fatal("dup2");
+
+    /* Wait for parent to grant us the tty if we are foreground. */
+    if (foreground && !ISSET(details->flags, CD_EXEC_BG)) {
+       struct timespec ts = { 0, 1000 };  /* 1us */
+       while (tcgetpgrp(io_fds[SFD_SLAVE]) != self)
+           nanosleep(&ts, NULL);
+    }
+
+    /* We have guaranteed that the slave fd is > 2 */
+    if (io_fds[SFD_SLAVE] != -1)
+       close(io_fds[SFD_SLAVE]);
+    if (io_fds[SFD_STDIN] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDIN]);
+    if (io_fds[SFD_STDOUT] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDOUT]);
+    if (io_fds[SFD_STDERR] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDERR]);
+
+    /* Execute command; only returns on error. */
+    exec_cmnd(details, errfd);
+
+    debug_return;
+}
+
+/*
+ * Fill in the monitor closure and setup initial events.
+ * Allocates read events for the signal pipe, error pipe and backchannel.
+ */
+static void
+fill_exec_closure_monitor(struct monitor_closure *mc,
+    struct command_status *cstat, int errfd, int backchannel)
+{
+    debug_decl(fill_exec_closure_monitor, SUDO_DEBUG_EXEC);
+    
+    /* Fill in the non-event part of the closure. */
+    cstat->type = CMD_INVALID;
+    cstat->val = 0;
+    mc->cstat = cstat;
+    mc->backchannel = backchannel;
+
+    /* Setup event base and events. */
+    mc->evbase = sudo_ev_base_alloc();
+    if (mc->evbase == NULL)
+        sudo_fatal(NULL);
+
+    /* Event for local signals via signal_pipe. */
+    mc->signal_pipe_event = sudo_ev_alloc(signal_pipe[0],
+       SUDO_EV_READ|SUDO_EV_PERSIST, mon_signal_pipe_cb, mc);
+    if (mc->signal_pipe_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(mc->evbase, mc->signal_pipe_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    /* Event for command status via errfd. */
+    mc->errpipe_event = sudo_ev_alloc(errfd,
+       SUDO_EV_READ|SUDO_EV_PERSIST, mon_errpipe_cb, mc);
+    if (mc->errpipe_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(mc->evbase, mc->errpipe_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    /* Event for forwarded signals via backchannel. */
+    mc->backchannel_event = sudo_ev_alloc(backchannel,
+       SUDO_EV_READ|SUDO_EV_PERSIST, mon_backchannel_cb, mc);
+    if (mc->backchannel_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(mc->evbase, mc->backchannel_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+}
+
+/*
+ * Monitor process that creates a new session with the controlling tty,
+ * resets signal handlers and forks a child to call exec_cmnd_pty().
+ * Waits for status changes from the command and relays them to the
+ * parent and relays signals from the parent to the command.
+ * Returns an error if fork(2) fails, else calls _exit(2).
+ */
+int
+exec_monitor(struct command_details *details, bool foreground, int backchannel)
+{
+    struct command_status cstat;
+    struct monitor_closure mc;
+    sigaction_t sa;
+    int errpipe[2], n;
+    debug_decl(exec_monitor, SUDO_DEBUG_EXEC);
+
+    /* Close unused fds. */
+    if (io_fds[SFD_MASTER] != -1)
+       close(io_fds[SFD_MASTER]);
+    if (io_fds[SFD_USERTTY] != -1)
+       close(io_fds[SFD_USERTTY]);
+
+    /*
+     * We use a pipe to atomically handle signal notification within
+     * the event loop.
+     */
+    if (pipe_nonblock(signal_pipe) != 0)
+       sudo_fatal(U_("unable to create pipe"));
+
+    /* Reset SIGWINCH and SIGALRM. */
+    memset(&sa, 0, sizeof(sa));
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = SA_RESTART;
+    sa.sa_handler = SIG_DFL;
+    if (sudo_sigaction(SIGWINCH, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGWINCH);
+    if (sudo_sigaction(SIGALRM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGALRM);
+
+    /* Ignore any SIGTTIN or SIGTTOU we get. */
+    sa.sa_handler = SIG_IGN;
+    if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN);
+    if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU);
+
+    /* Block all signals in mon_handler(). */
+    sigfillset(&sa.sa_mask);
+
+    /* Note: HP-UX poll() will not be interrupted if SA_RESTART is set. */
+    sa.sa_flags = SA_INTERRUPT;
+#ifdef SA_SIGINFO
+    sa.sa_flags |= SA_SIGINFO;
+    sa.sa_sigaction = mon_handler;
+#else
+    sa.sa_handler = mon_handler;
+#endif
+    if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
+
+    /* Catch common signals so we can cleanup properly. */
+    sa.sa_flags = SA_RESTART;
+#ifdef SA_SIGINFO
+    sa.sa_flags |= SA_SIGINFO;
+    sa.sa_sigaction = mon_handler;
+#else
+    sa.sa_handler = mon_handler;
+#endif
+    if (sudo_sigaction(SIGHUP, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGHUP);
+    if (sudo_sigaction(SIGINT, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGINT);
+    if (sudo_sigaction(SIGQUIT, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGQUIT);
+    if (sudo_sigaction(SIGTERM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTERM);
+    if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
+    if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR1);
+    if (sudo_sigaction(SIGUSR2, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR2);
+
+    /*
+     * Start a new session with the parent as the session leader
+     * and the slave pty as the controlling terminal.
+     * This allows us to be notified when the command has been suspended.
+     */
+    if (setsid() == -1) {
+       sudo_warn("setsid");
+       goto bad;
+    }
+    if (pty_make_controlling() == -1) {
+       sudo_warn(U_("unable to set controlling tty"));
+       goto bad;
+    }
+
+    mon_pgrp = getpgrp();      /* save a copy of our process group */
+
+    /* Start command and wait for it to stop or exit */
+    if (pipe(errpipe) == -1)
+       sudo_fatal(U_("unable to create pipe"));
+    cmnd_pid = sudo_debug_fork();
+    if (cmnd_pid == -1) {
+       sudo_warn(U_("unable to fork"));
+       goto bad;
+    }
+    if (cmnd_pid == 0) {
+       /* We pass errno back to our parent via pipe on exec failure. */
+       close(backchannel);
+       close(signal_pipe[0]);
+       close(signal_pipe[1]);
+       close(errpipe[0]);
+       (void)fcntl(errpipe[1], F_SETFD, FD_CLOEXEC);
+       restore_signals();
+
+       /* setup tty and exec command */
+       exec_cmnd_pty(details, foreground, errpipe[1]);
+       while (write(errpipe[1], &errno, sizeof(int)) == -1) {
+           if (errno != EINTR)
+               break;
+       }
+       _exit(1);
+    }
+    close(errpipe[1]);
+
+    /* No longer need execfd. */
+    if (details->execfd != -1) {
+       close(details->execfd);
+       details->execfd = -1;
+    }
+
+    /* Send the command's pid to main sudo process. */
+    cstat.type = CMD_PID;
+    cstat.val = cmnd_pid;
+    while (send(backchannel, &cstat, sizeof(cstat), 0) == -1) {
+       if (errno != EINTR)
+           break;
+    }
+
+    /* If any of stdin/stdout/stderr are pipes, close them in parent. */
+    if (io_fds[SFD_STDIN] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDIN]);
+    if (io_fds[SFD_STDOUT] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDOUT]);
+    if (io_fds[SFD_STDERR] != io_fds[SFD_SLAVE])
+       close(io_fds[SFD_STDERR]);
+
+    /* Put command in its own process group. */
+    cmnd_pgrp = cmnd_pid;
+    setpgid(cmnd_pid, cmnd_pgrp);
+
+    /* Make the command the foreground process for the pty slave. */
+    if (foreground && !ISSET(details->flags, CD_EXEC_BG)) {
+       do {
+           n = tcsetpgrp(io_fds[SFD_SLAVE], cmnd_pgrp);
+       } while (n == -1 && errno == EINTR);
+    }
+
+    /*
+     * Create new event base and register read events for the
+     * signal pipe, error pipe, and backchannel.
+     */
+    fill_exec_closure_monitor(&mc, &cstat, errpipe[0], backchannel);
+
+    /*
+     * Wait for errno on pipe, signal on backchannel or for SIGCHLD.
+     * The event loop ends when the child is no longer running and
+     * the error pipe is closed.
+     */
+    (void) sudo_ev_loop(mc.evbase, 0);
+    if (cmnd_pid != -1) {
+       /* XXX An error occurred, should send a message back. */
+       sudo_debug_printf(SUDO_DEBUG_ERROR,
+           "Command still running after event loop exit, sending SIGKILL");
+       kill(cmnd_pid, SIGKILL);
+       /* XXX - wait for cmnd_pid to exit */
+    } else {
+       /* Send parent status. */
+       send_status(backchannel, &cstat);
+    }
+
+#ifdef HAVE_SELINUX
+    if (ISSET(details->flags, CD_RBAC_ENABLED)) {
+       if (selinux_restore_tty() != 0)
+           sudo_warnx(U_("unable to restore tty label"));
+    }
+#endif
+    sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
+    _exit(1);
+
+bad:
+    debug_return_int(errno);
+}
diff --git a/src/exec_nopty.c b/src/exec_nopty.c
new file mode 100644 (file)
index 0000000..c72eb31
--- /dev/null
@@ -0,0 +1,505 @@
+/*
+ * Copyright (c) 2009-2017 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif /* HAVE_STRING_H */
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif /* HAVE_STRINGS_H */
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#include "sudo.h"
+#include "sudo_exec.h"
+#include "sudo_event.h"
+#include "sudo_plugin.h"
+#include "sudo_plugin_int.h"
+
+struct exec_closure_nopty {
+    pid_t child;
+    struct command_status *cstat;
+    struct command_details *details;
+    struct sudo_event_base *evbase;
+    struct sudo_event *signal_event;
+    struct sudo_event *errpipe_event;
+};
+
+static void signal_pipe_cb(int fd, int what, void *v);
+#ifdef SA_SIGINFO
+static void exec_handler_user_only(int s, siginfo_t *info, void *context);
+#endif
+
+/* Note: this is basically the same as mon_errpipe_cb() in exec_monitor.c */
+static void
+errpipe_cb(int fd, int what, void *v)
+{
+    struct exec_closure_nopty *ec = v;
+    ssize_t n;
+    int errval;
+    debug_decl(errpipe_cb, SUDO_DEBUG_EXEC)
+
+    /* read errno from child or EOF when command is executed. */
+    n = read(fd, &errval, sizeof(errval));
+    switch (n) {
+    case -1:
+       switch (errno) {
+       case EINTR:
+           /* got a signal, restart loop to service it. */
+           sudo_ev_loopcontinue(ec->evbase);
+           break;
+       case EAGAIN:
+           /* not ready after all... */
+           break;
+       default:
+           sudo_debug_printf(SUDO_DEBUG_ERROR,
+               "failed to read error pipe: %s", strerror(errno));
+           ec->cstat->type = CMD_ERRNO;
+           ec->cstat->val = errno;
+           sudo_ev_loopbreak(ec->evbase);
+           break;
+       }
+       break;
+    case 0:
+       /*
+        * We get EOF when the command is executed and the other
+        * end of the error pipe is closed.  Just remove the event.
+        */
+       sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error pipe, removing event");
+       sudo_ev_del(ec->evbase, ec->errpipe_event);
+       break;
+    default:
+       /* Errno value when child is unable to execute command. */
+       sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s",
+           strerror(errval));
+       ec->cstat->type = CMD_ERRNO;
+       ec->cstat->val = errval;
+       sudo_ev_del(ec->evbase, ec->errpipe_event);
+       break;
+    }
+    debug_return;
+}
+
+/*
+ * Fill in the exec closure and setup initial exec events.
+ * Allocates events for the signal pipe and error pipe.
+ */
+static void
+fill_exec_closure_nopty(struct exec_closure_nopty *ec,
+    struct command_status *cstat, struct command_details *details, int errfd)
+{
+    debug_decl(fill_exec_closure_nopty, SUDO_DEBUG_EXEC)
+
+    /* Fill in the non-event part of the closure. */
+    ec->child = cmnd_pid;
+    ec->cstat = cstat;
+    ec->details = details;
+
+    /* Setup event base and events. */
+    ec->evbase = sudo_ev_base_alloc();
+    if (ec->evbase == NULL)
+       sudo_fatal(NULL);
+
+    /* Event for local signals via signal_pipe. */
+    ec->signal_event = sudo_ev_alloc(signal_pipe[0],
+       SUDO_EV_READ|SUDO_EV_PERSIST, signal_pipe_cb, ec);
+    if (ec->signal_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(ec->evbase, ec->signal_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    /* Event for command status via errfd. */
+    ec->errpipe_event = sudo_ev_alloc(errfd,
+       SUDO_EV_READ|SUDO_EV_PERSIST, errpipe_cb, ec);
+    if (ec->errpipe_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(ec->evbase, ec->errpipe_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    sudo_debug_printf(SUDO_DEBUG_INFO, "signal pipe fd %d\n", signal_pipe[0]);
+    sudo_debug_printf(SUDO_DEBUG_INFO, "error pipe fd %d\n", errfd);
+
+    debug_return;
+}
+
+/*
+ * Execute a command and wait for it to finish.
+ */
+int
+exec_nopty(struct command_details *details, struct command_status *cstat)
+{
+    struct exec_closure_nopty ec;
+    sigaction_t sa;
+    int errpipe[2];
+    debug_decl(exec_nopty, SUDO_DEBUG_EXEC)
+
+    /*
+     * We use a pipe to get errno if execve(2) fails in the child.
+     */
+    if (pipe(errpipe) == -1)
+       sudo_fatal(U_("unable to create pipe"));
+
+    /*
+     * Signals to pass to the child process (excluding SIGALRM).
+     * We block all other signals while running the signal handler.
+     * Note: HP-UX select() will not be interrupted if SA_RESTART set.
+     *
+     * We also need to handle suspend/restore of sudo and the command.
+     * In most cases, the command will be in the same process group as
+     * sudo and job control will "just work".  However, if the command
+     * changes its process group ID and does not change it back (or is
+     * kill by SIGSTOP which is not catchable), we need to resume the
+     * command manually.  Also, if SIGTSTP is sent directly to sudo,
+     * we need to suspend the command, and then suspend ourself, restoring
+     * the default SIGTSTP handler temporarily.
+     *
+     * XXX - currently we send SIGCONT upon resume in some cases where
+     * we don't need to (e.g. command pgrp == parent pgrp).
+     */
+
+    memset(&sa, 0, sizeof(sa));
+    sigfillset(&sa.sa_mask);
+    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
+#ifdef SA_SIGINFO
+    sa.sa_flags |= SA_SIGINFO;
+    sa.sa_sigaction = exec_handler;
+#else
+    sa.sa_handler = exec_handler;
+#endif
+    if (sudo_sigaction(SIGTERM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTERM);
+    if (sudo_sigaction(SIGHUP, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGHUP);
+    if (sudo_sigaction(SIGALRM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGALRM);
+    if (sudo_sigaction(SIGPIPE, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGPIPE);
+    if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR1);
+    if (sudo_sigaction(SIGUSR2, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR2);
+    if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
+    if (sudo_sigaction(SIGCONT, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGCONT);
+#ifdef SIGINFO
+    if (sudo_sigaction(SIGINFO, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGINFO);
+#endif
+
+    /*
+     * When not running the command in a pty, we do not want to
+     * forward signals generated by the kernel that the child will
+     * already have received by virtue of being in the controlling
+     * terminals's process group (SIGINT, SIGQUIT, SIGTSTP).
+     */
+#ifdef SA_SIGINFO
+    sa.sa_flags |= SA_SIGINFO;
+    sa.sa_sigaction = exec_handler_user_only;
+#endif
+    if (sudo_sigaction(SIGINT, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGINT);
+    if (sudo_sigaction(SIGQUIT, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGQUIT);
+    if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
+
+    /*
+     * The policy plugin's session init must be run before we fork
+     * or certain pam modules won't be able to track their state.
+     */
+    if (policy_init_session(details) != true)
+       sudo_fatalx(U_("policy plugin failed session initialization"));
+
+    ppgrp = getpgrp(); /* parent's process group */
+
+    cmnd_pid = sudo_debug_fork();
+    switch (cmnd_pid) {
+    case -1:
+       sudo_fatal(U_("unable to fork"));
+       break;
+    case 0:
+       /* child */
+       close(errpipe[0]);
+       close(signal_pipe[0]);
+       close(signal_pipe[1]);
+       (void)fcntl(errpipe[1], F_SETFD, FD_CLOEXEC);
+       exec_cmnd(details, errpipe[1]);
+       while (write(errpipe[1], &errno, sizeof(int)) == -1) {
+           if (errno != EINTR)
+               break;
+       }
+       sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
+       _exit(1);
+    }
+    sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command,
+       (int)cmnd_pid);
+    close(errpipe[1]);
+
+    /* No longer need execfd. */
+    if (details->execfd != -1) {
+       close(details->execfd);
+       details->execfd = -1;
+    }
+
+    /* Set command timeout if specified. */
+    if (ISSET(details->flags, CD_SET_TIMEOUT))
+       alarm(details->timeout);
+
+    /*
+     * Fill in exec closure, allocate event base and two persistent events:
+     * the signal pipe and the error pipe.
+     */
+    fill_exec_closure_nopty(&ec, cstat, details, errpipe[0]);
+
+    /*
+     * Non-pty event loop.
+     * Wait for command to exit, handles signals and the error pipe.
+     */
+    if (sudo_ev_loop(ec.evbase, 0) == -1)
+       sudo_warn(U_("error in event loop"));
+    if (sudo_ev_got_break(ec.evbase)) {
+       /* error from callback */
+       sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely");
+       /* kill command */
+       terminate_command(ec.child, true);
+    }
+
+#ifdef HAVE_SELINUX
+    if (ISSET(details->flags, CD_RBAC_ENABLED)) {
+       if (selinux_restore_tty() != 0)
+           sudo_warnx(U_("unable to restore tty label"));
+    }
+#endif
+
+    /* Free things up. */
+    sudo_ev_base_free(ec.evbase);
+    sudo_ev_free(ec.signal_event);
+    sudo_ev_free(ec.errpipe_event);
+    debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0);
+}
+
+/*
+ * Forward a signal to the command (non-pty version) or handle
+ * changes to the command's status (SIGCHLD).
+ * XXX - separate SIGCHLD code?
+ */
+static void
+dispatch_signal(struct exec_closure_nopty *ec, int signo, char *signame)
+{
+    debug_decl(dispatch_signal, SUDO_DEBUG_EXEC)
+
+    sudo_debug_printf(SUDO_DEBUG_INFO,
+       "%s: evbase %p, child: %d, signo %s(%d), cstat %p",
+       __func__, ec->evbase, (int)ec->child, signame, signo, ec->cstat);
+
+    if (ec->child == -1)
+       goto done;
+
+    if (signo == SIGCHLD) {
+       pid_t pid;
+       int status;
+       /*
+        * The command stopped or exited.
+        */
+       do {
+           pid = waitpid(ec->child, &status, WUNTRACED|WNOHANG);
+       } while (pid == -1 && errno == EINTR);
+       if (pid == ec->child) {
+           if (WIFSTOPPED(status)) {
+               /*
+                * Save the controlling terminal's process group
+                * so we can restore it after we resume, if needed.
+                * Most well-behaved shells change the pgrp back to
+                * its original value before suspending so we must
+                * not try to restore in that case, lest we race with
+                * the child upon resume, potentially stopping sudo
+                * with SIGTTOU while the command continues to run.
+                */
+               sigaction_t sa, osa;
+               pid_t saved_pgrp = -1;
+               int signo = WSTOPSIG(status);
+               int fd = open(_PATH_TTY, O_RDWR);
+               if (fd != -1) {
+                   saved_pgrp = tcgetpgrp(fd);
+                   if (saved_pgrp == -1) {
+                       close(fd);
+                       fd = -1;
+                   }
+               }
+               if (saved_pgrp != -1) {
+                   /*
+                    * Child was stopped trying to access controlling
+                    * terminal.  If the child has a different pgrp
+                    * and we own the controlling terminal, give it
+                    * to the child's pgrp and let it continue.
+                    */
+                   if (signo == SIGTTOU || signo == SIGTTIN) {
+                       if (saved_pgrp == ppgrp) {
+                           pid_t child_pgrp = getpgid(ec->child);
+                           if (child_pgrp != ppgrp) {
+                               if (tcsetpgrp(fd, child_pgrp) == 0) {
+                                   if (killpg(child_pgrp, SIGCONT) != 0) {
+                                       sudo_warn("kill(%d, SIGCONT)",
+                                           (int)child_pgrp);
+                                   }
+                                   close(fd);
+                                   goto done;
+                               }
+                           }
+                       }
+                   }
+               }
+               if (signo == SIGTSTP) {
+                   memset(&sa, 0, sizeof(sa));
+                   sigemptyset(&sa.sa_mask);
+                   sa.sa_flags = SA_RESTART;
+                   sa.sa_handler = SIG_DFL;
+                   if (sudo_sigaction(SIGTSTP, &sa, &osa) != 0) {
+                       sudo_warn(U_("unable to set handler for signal %d"),
+                           SIGTSTP);
+                   }
+               }
+               if (kill(getpid(), signo) != 0)
+                   sudo_warn("kill(%d, SIG%s)", (int)getpid(), signame);
+               if (signo == SIGTSTP) {
+                   if (sudo_sigaction(SIGTSTP, &osa, NULL) != 0) {
+                       sudo_warn(U_("unable to restore handler for signal %d"),
+                           SIGTSTP);
+                   }
+               }
+               if (saved_pgrp != -1) {
+                   /*
+                    * Restore command's process group if different.
+                    * Otherwise, we cannot resume some shells.
+                    */
+                   if (saved_pgrp != ppgrp)
+                       (void)tcsetpgrp(fd, saved_pgrp);
+                   close(fd);
+               }
+           } else {
+               /* Child has exited or been killed, we are done. */
+               ec->child = -1;
+               /* Don't overwrite execve() failure with child exit status. */
+               if (ec->cstat->type != CMD_ERRNO) {
+                   ec->cstat->type = CMD_WSTATUS;
+                   ec->cstat->val = status;
+               }
+               sudo_ev_del(ec->evbase, ec->signal_event);
+               sudo_ev_loopexit(ec->evbase);
+               goto done;
+           }
+       }
+    } else {
+       /* Send signal to child. */
+       if (signo == SIGALRM) {
+           terminate_command(ec->child, false);
+       } else if (kill(ec->child, signo) != 0) {
+           sudo_warn("kill(%d, SIG%s)", (int)ec->child, signame);
+       }
+    }
+done:
+    debug_return;
+}
+
+/* Signal pipe callback */
+static void
+signal_pipe_cb(int fd, int what, void *v)
+{
+    struct exec_closure_nopty *ec = v;
+    char signame[SIG2STR_MAX];
+    unsigned char signo;
+    ssize_t nread;
+    debug_decl(signal_pipe_cb, SUDO_DEBUG_EXEC)
+
+    /* Process received signals until the child dies or the pipe is empty. */
+    do {
+       /* read signal pipe */
+       nread = read(fd, &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;
+           /* On error, store errno and break out of the event loop. */
+           if (errno != EAGAIN) {
+               ec->cstat->type = CMD_ERRNO;
+               ec->cstat->val = errno;
+               sudo_warn(U_("error reading from signal pipe"));
+               sudo_ev_loopbreak(ec->evbase);
+           }
+           break;
+       }
+       if (sig2str(signo, signame) == -1)
+           snprintf(signame, sizeof(signame), "%d", signo);
+       sudo_debug_printf(SUDO_DEBUG_DIAG, "received SIG%s", signame);
+       /* XXX - deliver vs. SIGCHLD? */
+       dispatch_signal(ec, signo, signame);
+    } while (ec->child != -1);
+    debug_return;
+}
+
+#ifdef SA_SIGINFO
+/*
+ * Generic handler for signals passed from parent -> child.
+ * The other end of signal_pipe is checked in the main event loop.
+ * This version is for the non-pty case and does not forward
+ * signals that are generated by the kernel.
+ */
+static void
+exec_handler_user_only(int s, siginfo_t *info, void *context)
+{
+    unsigned char signo = (unsigned char)s;
+
+    /*
+     * Only forward user-generated signals not sent by a process in
+     * the command's own process group.  Signals sent by the kernel
+     * may include SIGTSTP when the user presses ^Z.  Curses programs
+     * often trap ^Z and send SIGTSTP to their own pgrp, so we don't
+     * want to send an extra SIGTSTP.
+     */
+    if (!USER_SIGNALED(info))
+       return;
+    if (info->si_pid != 0) {
+       pid_t si_pgrp = getpgid(info->si_pid);
+       if (si_pgrp != -1) {
+           if (si_pgrp == ppgrp || si_pgrp == cmnd_pid)
+               return;
+       } else if (info->si_pid == cmnd_pid) {
+           return;
+       }
+    }
+
+    /*
+     * The pipe is non-blocking, if we overflow the kernel's pipe
+     * buffer we drop the signal.  This is not a problem in practice.
+     */
+    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
+       if (errno != EINTR)
+           break;
+    }
+}
+#endif /* SA_SIGINFO */
index 885b6547702911ee8e01cc7e06612e5c6b45975b..796aa7fefe7619b5851b67f047c31d8e8036fe2a 100644 (file)
@@ -18,7 +18,6 @@
 
 #include <sys/types.h>
 #include <sys/socket.h>
-#include <sys/time.h>
 #include <sys/wait.h>
 #include <sys/ioctl.h>
 #include <stdio.h>
 # include <strings.h>
 #endif /* HAVE_STRINGS_H */
 #include <unistd.h>
-#ifdef TIME_WITH_SYS_TIME
-# include <time.h>
-#endif
 #include <errno.h>
 #include <fcntl.h>
 #include <signal.h>
-#include <termios.h>
 
 #include "sudo.h"
 #include "sudo_event.h"
 #include "sudo_plugin.h"
 #include "sudo_plugin_int.h"
 
-#define SFD_STDIN      0
-#define SFD_STDOUT     1
-#define SFD_STDERR     2
-#define SFD_MASTER     3
-#define SFD_SLAVE      4
-#define SFD_USERTTY    5
-
 /* Evaluates to true if the event has /dev/tty as its fd. */
 #define USERTTY_EVENT(_ev)     (sudo_ev_get_fd((_ev)) == io_fds[SFD_USERTTY])
 
 #define TERM_COOKED    0
 #define TERM_RAW       1
 
+/* We keep a tailq of signals to forward to child. */
+struct sigforward {
+    TAILQ_ENTRY(sigforward) entries;
+    int signo;
+};
+TAILQ_HEAD(sigfwd_list, sigforward);
+
+struct exec_closure_pty {
+    pid_t child;
+    sigset_t *omask;
+    struct command_status *cstat;
+    struct command_details *details;
+    struct sudo_event_base *evbase;
+    struct sudo_event *signal_event;
+    struct sudo_event *sigfwd_event;
+    struct sudo_event *backchannel_event;
+    struct sigfwd_list sigfwd_list;
+};
+
 /*
  * I/O buffer with associated read/write events and a logging action.
  * Used to, e.g. pass data from the pty to the user's terminal
@@ -76,101 +83,46 @@ struct io_buffer {
 SLIST_HEAD(io_buffer_list, io_buffer);
 
 static char slavename[PATH_MAX];
-static bool foreground, pipeline, tty_initialized;
-static int io_fds[6] = { -1, -1, -1, -1, -1, -1};
+int io_fds[6] = { -1, -1, -1, -1, -1, -1}; /* XXX - sudo_exec.h? */
+static bool foreground, pipeline;
+static bool tty_initialized;
 static int ttymode = TERM_COOKED;
-static pid_t ppgrp, cmnd_pgrp, mon_pgrp;
 static sigset_t ttyblock;
 static struct io_buffer_list iobufs;
+static const char *utmp_user;
 
+static int fork_pty(struct command_details *details, int sv[], sigset_t *omask);
 static void del_io_events(bool nonblocking);
-static int exec_monitor(struct command_details *details, int backchannel);
-static void exec_pty(struct command_details *details,
-    struct command_status *cstat, int errfd);
 static void sigwinch(int s);
 static void sync_ttysize(int src, int dst);
-static void deliver_signal(pid_t pid, int signo, bool from_parent);
 static int safe_close(int fd);
 static void ev_free_by_fd(struct sudo_event_base *evbase, int fd);
 static void check_foreground(void);
+static void add_io_events(struct sudo_event_base *evbase);
 
 /*
  * Cleanup hook for sudo_fatal()/sudo_fatalx()
  */
-static void
+void
 pty_cleanup(void)
 {
     debug_decl(cleanup, SUDO_DEBUG_EXEC);
 
     if (!TAILQ_EMPTY(&io_plugins) && io_fds[SFD_USERTTY] != -1)
        sudo_term_restore(io_fds[SFD_USERTTY], false);
-#ifdef HAVE_SELINUX
-    selinux_restore_tty();
-#endif
-    utmp_logout(slavename, 0); /* XXX - only if CD_SET_UTMP */
+    if (utmp_user != NULL)
+       utmp_logout(slavename, 0);
 
     debug_return;
 }
 
-/*
- * Generic handler for signals recieved by the monitor process.
- * The other end of signal_pipe is checked in the monitor event loop.
- */
-#ifdef SA_SIGINFO
-static void
-mon_handler(int s, siginfo_t *info, void *context)
-{
-    unsigned char signo = (unsigned char)s;
-
-    /*
-     * If the signal came from the process group of the command we ran,
-     * do not forward it as we don't want the child to indirectly kill
-     * itself.  This can happen with, e.g., BSD-derived versions of
-     * reboot that call kill(-1, SIGTERM) to kill all other processes.
-     */
-    if (s != SIGCHLD && USER_SIGNALED(info) && info->si_pid != 0) {
-       pid_t si_pgrp = getpgid(info->si_pid);
-       if (si_pgrp != -1) {
-           if (si_pgrp == cmnd_pgrp)
-               return;
-       } else if (info->si_pid == cmnd_pid) {
-               return;
-       }
-    }
-
-    /*
-     * The pipe is non-blocking, if we overflow the kernel's pipe
-     * buffer we drop the signal.  This is not a problem in practice.
-     */
-    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
-       if (errno != EINTR)
-           break;
-    }
-}
-#else
-static void
-mon_handler(int s)
-{
-    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.
-     */
-    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
-       if (errno != EINTR)
-           break;
-    }
-}
-#endif
-
 /*
  * Allocate a pty if /dev/tty is a tty.
  * Fills in io_fds[SFD_USERTTY], io_fds[SFD_MASTER], io_fds[SFD_SLAVE]
  * and slavename globals.
  */
-void
-pty_setup(uid_t uid, const char *tty, const char *utmp_user)
+static void
+pty_setup(uid_t uid, const char *tty)
 {
     debug_decl(pty_setup, SUDO_DEBUG_EXEC);
 
@@ -190,6 +142,24 @@ pty_setup(uid_t uid, const char *tty, const char *utmp_user)
     debug_return;
 }
 
+int
+pty_make_controlling(void)
+{
+    if (io_fds[SFD_USERTTY] != -1) {
+#ifdef TIOCSCTTY
+       if (ioctl(io_fds[SFD_SLAVE], TIOCSCTTY, NULL) != 0)
+           return -1;
+#else
+       /* Set controlling tty by reopening slave. */
+       int fd = open(slavename, O_RDWR);
+       if (fd == -1)
+           return -1;
+       close(fd);
+#endif
+    }
+    return 0;
+}
+
 /* Call I/O plugin tty input log method. */
 static bool
 log_ttyin(const char *buf, unsigned int n, struct io_buffer *iob)
@@ -409,13 +379,13 @@ check_foreground(void)
  * Returns SIGCONT_FG if the command should be resumed in the
  * foreground or SIGCONT_BG if it is a background process.
  */
-int
-suspend_parent(int signo)
+static int
+suspend_sudo(int signo)
 {
     char signame[SIG2STR_MAX];
     sigaction_t sa, osa;
     int ret = 0;
-    debug_decl(suspend_parent, SUDO_DEBUG_EXEC);
+    debug_decl(suspend_sudo, SUDO_DEBUG_EXEC);
 
     switch (signo) {
     case SIGTTOU:
@@ -495,42 +465,6 @@ suspend_parent(int signo)
     debug_return_int(ret);
 }
 
-/*
- * Kill command with increasing urgency.
- */
-void
-terminate_command(pid_t pid, bool use_pgrp)
-{
-    debug_decl(terminate_command, SUDO_DEBUG_EXEC);
-
-    /* Avoid killing more than a single process or process group. */
-    if (pid <= 0)
-       debug_return;
-
-    /*
-     * Note that SIGCHLD will interrupt the sleep()
-     */
-    if (use_pgrp) {
-       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGHUP", (int)pid);
-       killpg(pid, SIGHUP);
-       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGTERM", (int)pid);
-       killpg(pid, SIGTERM);
-       sleep(2);
-       sudo_debug_printf(SUDO_DEBUG_INFO, "killpg %d SIGKILL", (int)pid);
-       killpg(pid, SIGKILL);
-    } else {
-       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGHUP", (int)pid);
-       kill(pid, SIGHUP);
-       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGTERM", (int)pid);
-       kill(pid, SIGTERM);
-       sleep(2);
-       sudo_debug_printf(SUDO_DEBUG_INFO, "kill %d SIGKILL", (int)pid);
-       kill(pid, SIGKILL);
-    }
-
-    debug_return;
-}
-
 /*
  * Read an iobuf that is ready.
  */
@@ -705,7 +639,7 @@ io_buf_new(int rfd, int wfd,
  * process with std{in,out,err} hooked up to the pty or pipes as appropriate.
  * Returns the child pid.
  */
-int
+static int
 fork_pty(struct command_details *details, int sv[], sigset_t *omask)
 {
     struct command_status cstat;
@@ -791,27 +725,6 @@ fork_pty(struct command_details *details, int sv[], sigset_t *omask)
        io_fds[SFD_STDERR] = io_pipe[STDERR_FILENO][1];
     }
 
-    /* We don't want to receive SIGTTIN/SIGTTOU, getting EIO is preferable. */
-    sa.sa_handler = SIG_IGN;
-    if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN);
-    if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU);
-
-    /* Job control signals to relay from parent to child. */
-    sigfillset(&sa.sa_mask);
-    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
-#ifdef SA_SIGINFO
-    sa.sa_flags |= SA_SIGINFO;
-    sa.sa_sigaction = handler;
-#else
-    sa.sa_handler = handler;
-#endif
-    if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
-    if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
-
     if (foreground) {
        /* Copy terminal attrs from user tty -> pty slave. */
        if (sudo_term_copy(io_fds[SFD_USERTTY], io_fds[SFD_SLAVE])) {
@@ -856,10 +769,19 @@ fork_pty(struct command_details *details, int sv[], sigset_t *omask)
            close(io_pipe[STDOUT_FILENO][0]);
        if (io_pipe[STDERR_FILENO][0])
            close(io_pipe[STDERR_FILENO][0]);
-       exec_monitor(details, sv[1]);
+       /*                      
+        * If stdin/stdout is not a tty, start command in the background
+        * since it might be part of a pipeline that reads from /dev/tty.
+        * In this case, we rely on the command receiving SIGTTOU or SIGTTIN
+        * when it needs access to the controlling tty.
+        */                                                              
+       exec_monitor(details, foreground && !pipeline, sv[1]);
        cstat.type = CMD_ERRNO;
        cstat.val = errno;
-       ignore_result(send(sv[1], &cstat, sizeof(cstat), 0));
+       while (send(sv[1], &cstat, sizeof(cstat), 0) == -1) {
+           if (errno != EINTR)
+               break;
+       }
        _exit(1);
     }
 
@@ -874,7 +796,7 @@ fork_pty(struct command_details *details, int sv[], sigset_t *omask)
     debug_return_int(child);
 }
 
-void
+static void
 pty_close(struct command_status *cstat)
 {
     struct io_buffer *iob;
@@ -913,540 +835,420 @@ pty_close(struct command_status *cstat)
            n = io_fds[SFD_USERTTY] != -1 ?
                io_fds[SFD_USERTTY] : STDOUT_FILENO;
            if (write(n, reason, strlen(reason)) != -1) {
-               if (WCOREDUMP(cstat->val)) {
+               if (WCOREDUMP(cstat->val))
                    ignore_result(write(n, " (core dumped)", 14));
-               }
                ignore_result(write(n, "\n", 1));
            }
        }
     }
-    utmp_logout(slavename, cstat->type == CMD_WSTATUS ? cstat->val : 0); /* XXX - only if CD_SET_UTMP */
+    if (utmp_user != NULL)
+       utmp_logout(slavename, cstat->type == CMD_WSTATUS ? cstat->val : 0);
     debug_return;
 }
 
 /*
- * Schedule I/O events before starting the main event loop or
- * resuming from suspend.
+ * Schedule a signal to be forwarded.
  */
-void
-add_io_events(struct sudo_event_base *evbase)
+static void
+schedule_signal(struct exec_closure_pty *ec, int signo)
 {
-    struct io_buffer *iob;
-    debug_decl(add_io_events, SUDO_DEBUG_EXEC);
+    struct sigforward *sigfwd;
+    char signame[SIG2STR_MAX];
+    debug_decl(schedule_signal, SUDO_DEBUG_EXEC)
+
+    if (signo == SIGCONT_FG)
+       strlcpy(signame, "CONT_FG", sizeof(signame));
+    else if (signo == SIGCONT_BG)
+       strlcpy(signame, "CONT_BG", sizeof(signame));
+    else if (sig2str(signo, signame) == -1)
+       snprintf(signame, sizeof(signame), "%d", signo);
+    sudo_debug_printf(SUDO_DEBUG_DIAG, "scheduled SIG%s for child", signame);
+
+    if ((sigfwd = calloc(1, sizeof(*sigfwd))) == NULL)
+       sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+    sigfwd->signo = signo;
+    TAILQ_INSERT_TAIL(&ec->sigfwd_list, sigfwd, entries);
+
+    if (sudo_ev_add(ec->evbase, ec->sigfwd_event, NULL, true) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-    /*
-     * Schedule all readers as long as the buffer is not full.
-     * Schedule writers that contain buffered data.
-     * Normally, write buffers are added on demand when data is read.
-     */
-    SLIST_FOREACH(iob, &iobufs, entries) {
-       /* Don't read/write from /dev/tty if we are not in the foreground. */
-       if (iob->revent != NULL &&
-           (ttymode == TERM_RAW || !USERTTY_EVENT(iob->revent))) {
-           if (iob->len != sizeof(iob->buf)) {
-               sudo_debug_printf(SUDO_DEBUG_INFO,
-                   "added I/O revent %p, fd %d, events %d",
-                   iob->revent, iob->revent->fd, iob->revent->events);
-               if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1)
-                   sudo_fatal(U_("unable to add event to queue"));
-           }
-       }
-       if (iob->wevent != NULL &&
-           (foreground || !USERTTY_EVENT(iob->wevent))) {
-           if (iob->len > iob->off) {
-               sudo_debug_printf(SUDO_DEBUG_INFO,
-                   "added I/O wevent %p, fd %d, events %d",
-                   iob->wevent, iob->wevent->fd, iob->wevent->events);
-               if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
-                   sudo_fatal(U_("unable to add event to queue"));
-           }
-       }
-    }
     debug_return;
 }
 
-/*
- * Flush any output buffered in iobufs or readable from fds other
- * than /dev/tty.  Removes I/O events from the event base when done.
- */
 static void
-del_io_events(bool nonblocking)
+backchannel_cb(int fd, int what, void *v)
 {
-    struct io_buffer *iob;
-    struct sudo_event_base *evbase;
-    debug_decl(del_io_events, SUDO_DEBUG_EXEC);
+    struct exec_closure_pty *ec = v;
+    ssize_t n;
+    debug_decl(backchannel_cb, SUDO_DEBUG_EXEC)
 
-    /* Remove iobufs from existing event base. */
-    SLIST_FOREACH(iob, &iobufs, entries) {
-       if (iob->revent != NULL) {
-           sudo_debug_printf(SUDO_DEBUG_INFO,
-               "deleted I/O revent %p, fd %d, events %d",
-               iob->revent, iob->revent->fd, iob->revent->events);
-           sudo_ev_del(NULL, iob->revent);
+    /* read child status */
+    n = recv(fd, ec->cstat, sizeof(struct command_status), MSG_WAITALL);
+    if (n != sizeof(struct command_status)) {
+       if (n == -1) {
+           switch (errno) {
+           case EINTR:
+               /* got a signal, restart loop to service it. */
+               sudo_ev_loopcontinue(ec->evbase);
+               break;
+           case EAGAIN:
+               /* not ready after all... */
+               break;
+           default:
+               ec->cstat->type = CMD_ERRNO;
+               ec->cstat->val = errno;
+               sudo_debug_printf(SUDO_DEBUG_ERROR,
+                   "failed to read child status: %s", strerror(errno));
+               sudo_ev_loopbreak(ec->evbase);
+               break;
+           }
+       } else {
+           /* Short read or EOF. */
+           sudo_debug_printf(SUDO_DEBUG_ERROR,
+               "failed to read child status: %s", n ? "short read" : "EOF");
+           /* XXX - need new CMD_ type for monitor errors. */
+           errno = n ? EIO : ECONNRESET;
+           ec->cstat->type = CMD_ERRNO;
+           ec->cstat->val = errno;
+           sudo_ev_loopbreak(ec->evbase);
        }
-       if (iob->wevent != NULL) {
+       debug_return;
+    }
+    switch (ec->cstat->type) {
+    case CMD_PID:
+       /*
+        * Once we know the command's pid we can unblock
+        * signals which ere blocked in fork_pty().  This
+        * avoids a race between exec of the command and
+        * receipt of a fatal signal from it.
+        */
+       cmnd_pid = ec->cstat->val;
+       sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d",
+           ec->details->command, (int)cmnd_pid);
+       sigprocmask(SIG_SETMASK, ec->omask, NULL);
+       break;
+    case CMD_WSTATUS:
+       if (WIFSTOPPED(ec->cstat->val)) {
+           /* Suspend parent and tell child how to resume on return. */
            sudo_debug_printf(SUDO_DEBUG_INFO,
-               "deleted I/O wevent %p, fd %d, events %d",
-               iob->wevent, iob->wevent->fd, iob->wevent->events);
-           sudo_ev_del(NULL, iob->wevent);
+               "child stopped, suspending parent");
+           n = suspend_sudo(WSTOPSIG(ec->cstat->val));
+           schedule_signal(ec, n);
+           /* Re-enable I/O events and restart event loop to service signal. */
+           add_io_events(ec->evbase);
+           sudo_ev_loopcontinue(ec->evbase);
+       } else {
+           /* Child exited or was killed, either way we are done. */
+           sudo_debug_printf(SUDO_DEBUG_INFO, "child exited or was killed");
+           sudo_ev_loopexit(ec->evbase);
        }
+       break;
+    case CMD_ERRNO:
+       /* Child was unable to execute command or broken pipe. */
+       sudo_debug_printf(SUDO_DEBUG_INFO, "errno from child: %s",
+           strerror(ec->cstat->val));
+       sudo_ev_loopbreak(ec->evbase);
+       break;
     }
+    debug_return;
+}
 
-    /* Create temporary event base for flushing. */
-    evbase = sudo_ev_base_alloc();
-    if (evbase == NULL)
-       sudo_fatal(NULL);
+/*
+ * Forward a signal to the monitor (pty version) or handle
+ * changes to the monitors's status (SIGCHLD).
+ */
+static void
+dispatch_signal_pty(struct exec_closure_pty *ec, int signo, char *signame)
+{
+    debug_decl(dispatch_signal_pty, SUDO_DEBUG_EXEC)
 
-    /* Avoid reading from /dev/tty, just flush existing data. */
-    SLIST_FOREACH(iob, &iobufs, entries) {
-       /* Don't read from /dev/tty while flushing. */
-       if (iob->revent != NULL && !USERTTY_EVENT(iob->revent)) {
-           if (iob->len != sizeof(iob->buf)) {
-               if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1)
-                   sudo_fatal(U_("unable to add event to queue"));
-           }
-       }
-       /* Flush any write buffers with data in them. */
-       if (iob->wevent != NULL) {
-           if (iob->len > iob->off) {
-               if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
-                   sudo_fatal(U_("unable to add event to queue"));
-           }
-       }
-    }
-    (void) sudo_ev_loop(evbase, SUDO_EVLOOP_NONBLOCK);
+    sudo_debug_printf(SUDO_DEBUG_INFO,
+       "%s: evbase %p, child: %d, signo %s(%d), cstat %p",
+       __func__, ec->evbase, (int)ec->child, signame, signo, ec->cstat);
 
-    /*
-     * If not in non-blocking mode, make sure we flush write buffers.
-     * We don't want to read from the pty or stdin since that might block
-     * and the command is no longer running anyway.
-     */
-    if (!nonblocking) {
-       /* Clear out iobufs from event base. */
-       SLIST_FOREACH(iob, &iobufs, entries) {
-           if (iob->revent != NULL && !USERTTY_EVENT(iob->revent))
-               sudo_ev_del(evbase, iob->revent);
-           if (iob->wevent != NULL)
-               sudo_ev_del(evbase, iob->wevent);
-       }
+    if (ec->child == -1)
+       goto done;
 
-       SLIST_FOREACH(iob, &iobufs, entries) {
-           /* Flush any write buffers with data in them. */
-           if (iob->wevent != NULL) {
-               if (iob->len > iob->off) {
-                   if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
-                       sudo_fatal(U_("unable to add event to queue"));
-               }
-           }
-       }
-       (void) sudo_ev_loop(evbase, 0);
-     
-       /* We should now have flushed all write buffers. */
-       SLIST_FOREACH(iob, &iobufs, entries) {
-           if (iob->wevent != NULL) {
-               if (iob->len > iob->off) {
-                   sudo_debug_printf(SUDO_DEBUG_ERROR,
-                       "unflushed data: wevent %p, fd %d, events %d",
-                       iob->wevent, iob->wevent->fd, iob->wevent->events);
-               }
+    if (signo == SIGCHLD) {
+       int n, status;
+       pid_t pid;
+       /*
+        * Monitor process was signaled; wait for it as needed.
+        */
+       do {
+           pid = waitpid(ec->child, &status, WUNTRACED|WNOHANG);
+       } while (pid == -1 && errno == EINTR);
+       if (pid == ec->child) {
+           /*
+            * If the monitor dies we get notified via backchannel_cb().
+            * If it was stopped, we should stop too (the command keeps
+            * running in its pty) and continue it when we come back.
+            */
+           if (WIFSTOPPED(status)) {
+               sudo_debug_printf(SUDO_DEBUG_INFO,
+                   "monitor stopped, suspending parent");
+               n = suspend_sudo(WSTOPSIG(status));
+               kill(pid, SIGCONT);
+               schedule_signal(ec, n);
+               /* Re-enable I/O events and restart event loop. */
+               add_io_events(ec->evbase);
+               sudo_ev_loopcontinue(ec->evbase);
+               goto done;
+           } else if (WIFSIGNALED(status)) {
+               sudo_debug_printf(SUDO_DEBUG_INFO,
+                   "monitor killed, signal %d", WTERMSIG(status));
+               ec->child = -1;
+           } else {
+               sudo_debug_printf(SUDO_DEBUG_INFO,
+                   "monitor exited, status %d", WEXITSTATUS(status));
+               ec->child = -1;
            }
        }
+    } else {
+       /* Schedule signo to be forwared to the child. */
+       schedule_signal(ec, signo);
+       /* Restart event loop to service signal immediately. */
+       sudo_ev_loopcontinue(ec->evbase);
     }
-
-    /* Free temporary event base, removing its events. */
-    sudo_ev_base_free(evbase);
-
+done:
     debug_return;
 }
 
+/* Signal pipe callback */
 static void
-deliver_signal(pid_t pid, int signo, bool from_parent)
+signal_pipe_cb(int fd, int what, void *v)
 {
+    struct exec_closure_pty *ec = v;
     char signame[SIG2STR_MAX];
-    int status;
-    debug_decl(deliver_signal, SUDO_DEBUG_EXEC);
-
-    /* Avoid killing more than a single process or process group. */
-    if (pid <= 0)
-       debug_return;
+    unsigned char signo;
+    ssize_t nread;
+    debug_decl(signal_pipe_cb, SUDO_DEBUG_EXEC)
 
-    if (signo == SIGCONT_FG)
-       strlcpy(signame, "CONT_FG", sizeof(signame));
-    else if (signo == SIGCONT_BG)
-       strlcpy(signame, "CONT_BG", sizeof(signame));
-    else if (sig2str(signo, signame) == -1)
-       snprintf(signame, sizeof(signame), "%d", signo);
-
-    /* Handle signal from parent. */
-    sudo_debug_printf(SUDO_DEBUG_INFO, "received SIG%s%s",
-       signame, from_parent ? " from parent" : "");
-    switch (signo) {
-    case SIGALRM:
-       terminate_command(pid, true);
-       break;
-    case SIGCONT_FG:
-       /* Continue in foreground, grant it controlling tty. */
-       do {
-           status = tcsetpgrp(io_fds[SFD_SLAVE], cmnd_pgrp);
-       } while (status == -1 && errno == EINTR);
-       killpg(pid, SIGCONT);
-       break;
-    case SIGCONT_BG:
-       /* Continue in background, I take controlling tty. */
-       do {
-           status = tcsetpgrp(io_fds[SFD_SLAVE], mon_pgrp);
-       } while (status == -1 && errno == EINTR);
-       killpg(pid, SIGCONT);
-       break;
-    case SIGKILL:
-       _exit(1); /* XXX */
-       /* NOTREACHED */
-    default:
-       /* Relay signal to command. */
-       killpg(pid, signo);
-       break;
-    }
+    do {
+       /* read signal pipe */
+       nread = read(fd, &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;
+           /* On error, store errno and break out of the event loop. */
+           if (errno != EAGAIN) {
+               ec->cstat->type = CMD_ERRNO;
+               ec->cstat->val = errno;
+               sudo_warn(U_("error reading from signal pipe"));
+               sudo_ev_loopbreak(ec->evbase);
+           }
+           break;
+       }
+       if (sig2str(signo, signame) == -1)
+           snprintf(signame, sizeof(signame), "%d", signo);
+       sudo_debug_printf(SUDO_DEBUG_DIAG, "received SIG%s", signame);
+       dispatch_signal_pty(ec, signo, signame);
+    } while (ec->child != -1);
     debug_return;
 }
 
 /*
- * Send status to parent over socketpair.
- * Return value is the same as send(2).
+ * Forward signals in sigfwd_list to the monitor so it can
+ * deliver them to the command.
  */
-static int
-send_status(int fd, struct command_status *cstat)
+static void
+sigfwd_cb(int sock, int what, void *v)
 {
-    int n = -1;
-    debug_decl(send_status, SUDO_DEBUG_EXEC);
-
-    if (cstat->type != CMD_INVALID) {
+    struct exec_closure_pty *ec = v;
+    char signame[SIG2STR_MAX];
+    struct sigforward *sigfwd;
+    struct command_status cstat;
+    ssize_t nsent;
+    debug_decl(sigfwd_cb, SUDO_DEBUG_EXEC)
+
+    while (!TAILQ_EMPTY(&ec->sigfwd_list)) {
+       sigfwd = TAILQ_FIRST(&ec->sigfwd_list);
+       if (sigfwd->signo == SIGCONT_FG)
+           strlcpy(signame, "CONT_FG", sizeof(signame));
+       else if (sigfwd->signo == SIGCONT_BG)
+           strlcpy(signame, "CONT_BG", sizeof(signame));
+       else if (sig2str(sigfwd->signo, signame) == -1)
+           snprintf(signame, sizeof(signame), "%d", sigfwd->signo);
        sudo_debug_printf(SUDO_DEBUG_INFO,
-           "sending status message to parent: [%d, %d]",
-           cstat->type, cstat->val);
+           "sending SIG%s to child over backchannel", signame);
+       cstat.type = CMD_SIGNO;
+       cstat.val = sigfwd->signo;
        do {
-           n = send(fd, cstat, sizeof(*cstat), 0);
-       } while (n == -1 && errno == EINTR);
-       if (n != sizeof(*cstat)) {
-           sudo_debug_printf(SUDO_DEBUG_ERROR,
-               "unable to send status to parent: %s", strerror(errno));
+           nsent = send(sock, &cstat, sizeof(cstat), 0);
+       } while (nsent == -1 && errno == EINTR);
+       TAILQ_REMOVE(&ec->sigfwd_list, sigfwd, entries);
+       free(sigfwd);
+       if (nsent != sizeof(cstat)) {
+           if (errno == EPIPE) {
+               struct sigforward *sigfwd_next;
+               sudo_debug_printf(SUDO_DEBUG_ERROR,
+                   "broken pipe writing to child over backchannel");
+               /* Other end of socket gone, empty out sigfwd_list. */
+               TAILQ_FOREACH_SAFE(sigfwd, &ec->sigfwd_list, entries, sigfwd_next) {
+                   free(sigfwd);
+               }
+               TAILQ_INIT(&ec->sigfwd_list);
+               /* XXX - child (monitor) is dead, we should exit too? */
+           }
+           break;
        }
-       cstat->type = CMD_INVALID; /* prevent re-sending */
     }
-    debug_return_int(n);
 }
 
 /*
- * Wait for command status after receiving SIGCHLD.
- * If the command was stopped, the status is send back to the parent.
- * Otherwise, cstat is filled in but not sent.
+ * Fill in the exec closure and setup initial exec events.
+ * Allocates events for the signal pipe and backchannel.
+ * Forwarded signals on the backchannel are enabled on demand.
  */
 static void
-handle_sigchld(int backchannel, struct command_status *cstat)
+fill_exec_closure_pty(struct exec_closure_pty *ec, struct command_status *cstat,
+    struct command_details *details, pid_t child, sigset_t *omask,
+    int backchannel)
 {
-    char signame[SIG2STR_MAX];
-    int status;
-    pid_t pid;
-    debug_decl(handle_sigchld, SUDO_DEBUG_EXEC);
-
-    /* Read command status. */
-    do {
-       pid = waitpid(cmnd_pid, &status, WUNTRACED|WCONTINUED|WNOHANG);
-    } while (pid == -1 && errno == EINTR);
-    switch (pid) {
-    case 0:
-       errno = ECHILD;
-       /* FALLTHROUGH */
-    case -1:
-       sudo_debug_printf(SUDO_DEBUG_DIAG,
-           "waitpid returned %d, expected pid %d", pid, cmnd_pid);
-       sudo_warn(U_("%s: %s"), __func__, "waitpid");
-       debug_return;
-    }
-
-    if (WIFCONTINUED(status)) {
-       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) resumed",
-           __func__, cmnd_pid);
-    } else if (WIFSTOPPED(status)) {
-       if (sig2str(WSTOPSIG(status), signame) == -1)
-           snprintf(signame, sizeof(signame), "%d", WSTOPSIG(status));
-       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s",
-           __func__, cmnd_pid, signame);
-    } else if (WIFSIGNALED(status)) {
-       if (sig2str(WTERMSIG(status), signame) == -1)
-           snprintf(signame, sizeof(signame), "%d", WTERMSIG(status));
-       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) killed, SIG%s",
-           __func__, cmnd_pid, signame);
-       cmnd_pid = -1;
-    } else if (WIFEXITED(status)) {
-       sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) exited: %d",
-           __func__, cmnd_pid, WEXITSTATUS(status));
-       cmnd_pid = -1;
-    } else {
-       sudo_debug_printf(SUDO_DEBUG_WARN,
-           "%s: unexpected wait status %d for command (%d)",
-           __func__, status, (int)cmnd_pid);
-    }
-
-    /* Don't overwrite execve() failure with child exit status. */
-    if (cstat->type != CMD_ERRNO) {
-       /*
-        * Store wait status in cstat and forward to parent if stopped.
-        */
-       cstat->type = CMD_WSTATUS;
-       cstat->val = status;
-       if (WIFSTOPPED(status)) {
-           /* Save the foreground pgid so we can restore it later. */
-           do {
-               pid = tcgetpgrp(io_fds[SFD_SLAVE]);
-           } while (pid == -1 && errno == EINTR);
-           if (pid != mon_pgrp)
-               cmnd_pgrp = pid;
-           send_status(backchannel, cstat);
-       }
-    }
-
-    debug_return;
-}
-
-struct monitor_closure {
-    struct sudo_event_base *evbase;
-    struct sudo_event *errpipe_event;
-    struct sudo_event *backchannel_event;
-    struct sudo_event *signal_pipe_event;
-    struct command_status *cstat;
-    int backchannel;
-};
-
-static void
-mon_signal_pipe_cb(int fd, int what, void *v)
-{
-    struct monitor_closure *mc = v;
-    unsigned char signo;
-    ssize_t nread;
-    debug_decl(mon_signal_pipe_cb, SUDO_DEBUG_EXEC);
-
-    nread = read(fd, &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_warn(U_("error reading from signal pipe"));
-           sudo_ev_loopbreak(mc->evbase);
-       }
-    } else {
-       /*
-        * Handle SIGCHLD specially and deliver other signals
-        * directly to the command.
-        */
-       if (signo == SIGCHLD) {
-           handle_sigchld(mc->backchannel, mc->cstat);
-           if (cmnd_pid == -1) {
-               /* Remove all but the errpipe event. */
-               sudo_ev_del(mc->evbase, mc->backchannel_event);
-               sudo_ev_del(mc->evbase, mc->signal_pipe_event);
-           }
-       } else {
-           deliver_signal(cmnd_pid, signo, false);
-       }
-    }
-    debug_return;
-}
+    debug_decl(fill_exec_closure_pty, SUDO_DEBUG_EXEC)
+
+    /* Fill in the non-event part of the closure. */
+    ec->child = child;
+    ec->omask = omask;
+    ec->cstat = cstat;
+    ec->details = details;
+    TAILQ_INIT(&ec->sigfwd_list);
+
+    /* Setup event base and events. */
+    ec->evbase = sudo_ev_base_alloc();
+    if (ec->evbase == NULL)
+       sudo_fatal(NULL);
 
-static void
-mon_errpipe_cb(int fd, int what, void *v)
-{
-    struct monitor_closure *mc = v;
-    ssize_t n;
-    debug_decl(mon_errpipe_cb, SUDO_DEBUG_EXEC);
+    /* Event for local signals via signal_pipe. */
+    ec->signal_event = sudo_ev_alloc(signal_pipe[0],
+       SUDO_EV_READ|SUDO_EV_PERSIST, signal_pipe_cb, ec);
+    if (ec->signal_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(ec->evbase, ec->signal_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-    /* read errno or EOF from command pipe */
-    n = read(fd, mc->cstat, sizeof(struct command_status));
-    if (n == -1) {
-       if (errno != EINTR && errno != EAGAIN) {
-           sudo_warn(U_("error reading from pipe"));
-           sudo_ev_loopbreak(mc->evbase);
-       }
-    } else {
-       /* Got errno or EOF, either way we are done with errpipe. */
-       sudo_debug_printf(SUDO_DEBUG_DIAG, "%s: type: %d, val: %d",
-           __func__, mc->cstat->type, mc->cstat->val);
-       sudo_ev_del(mc->evbase, mc->errpipe_event);
-       close(fd);
-    }
-    debug_return;
-}
+    /* Event for command status via backchannel. */
+    ec->backchannel_event = sudo_ev_alloc(backchannel,
+       SUDO_EV_READ|SUDO_EV_PERSIST, backchannel_cb, ec);
+    if (ec->backchannel_event == NULL)
+       sudo_fatal(NULL);
+    if (sudo_ev_add(ec->evbase, ec->backchannel_event, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-static void
-mon_backchannel_cb(int fd, int what, void *v)
-{
-    struct monitor_closure *mc = v;
-    struct command_status cstmp;
-    ssize_t n;
-    debug_decl(mon_backchannel_cb, SUDO_DEBUG_EXEC);
+    /* The signal forwarding event gets added on demand. */
+    ec->sigfwd_event = sudo_ev_alloc(backchannel,
+       SUDO_EV_WRITE, sigfwd_cb, ec);
+    if (ec->sigfwd_event == NULL)
+       sudo_fatal(NULL);
 
-    /* Read command from backchannel, should be a signal. */
-    n = recv(fd, &cstmp, sizeof(cstmp), MSG_WAITALL);
-    if (n != sizeof(cstmp)) {
-       if (n == -1) {
-           if (errno == EINTR || errno == EAGAIN)
-               debug_return;
-           sudo_warn(U_("error reading from socketpair"));
-       } else {
-           /* short read or EOF, parent process died? */
-       }
-       sudo_ev_loopbreak(mc->evbase);
-    } else {
-       if (cstmp.type == CMD_SIGNO) {
-           deliver_signal(cmnd_pid, cstmp.val, true);
-       } else {
-           sudo_warnx(U_("unexpected reply type on backchannel: %d"), cstmp.type);
-       }
-    }
-    debug_return;
+    sudo_debug_printf(SUDO_DEBUG_INFO, "signal pipe fd %d\n", signal_pipe[0]);
+    sudo_debug_printf(SUDO_DEBUG_INFO, "backchannel fd %d\n", backchannel);
 }
 
 /*
- * Monitor process that creates a new session with the controlling tty,
- * resets signal handlers and forks a child to call exec_pty().
- * Waits for status changes from the command and relays them to the
- * parent and relays signals from the parent to the command.
- * Returns an error if fork(2) fails, else calls _exit(2).
+ * Execute a command in a pty, potentially with I/O loggging, and
+ * wait for it to finish.
+ * This is a little bit tricky due to how POSIX job control works and
+ * we fact that we have two different controlling terminals to deal with.
  */
-static int
-exec_monitor(struct command_details *details, int backchannel)
+int
+exec_pty(struct command_details *details, struct command_status *cstat)
 {
-    struct command_status cstat;
-    struct sudo_event_base *evbase;
-    struct monitor_closure mc;
+    struct sigforward *sigfwd, *sigfwd_next;
+    struct exec_closure_pty ec;
     sigaction_t sa;
-    int errpipe[2], n;
-    debug_decl(exec_monitor, SUDO_DEBUG_EXEC);
+    sigset_t omask;
+    pid_t child;
+    int sv[2];
+    debug_decl(exec_pty, SUDO_DEBUG_EXEC)
 
-    /* Close unused fds. */
-    if (io_fds[SFD_MASTER] != -1)
-       close(io_fds[SFD_MASTER]);
-    if (io_fds[SFD_USERTTY] != -1)
-       close(io_fds[SFD_USERTTY]);
+    /*
+     * Allocate a pty.
+     */
+    if (ISSET(details->flags, CD_SET_UTMP))
+       utmp_user = details->utmp_user ? details->utmp_user : user_details.username;
+    sudo_debug_printf(SUDO_DEBUG_INFO, "allocate pty for I/O logging");
+    pty_setup(details->euid, user_details.tty);
 
     /*
-     * We use a pipe to atomically handle signal notification within
-     * the event loop.
+     * 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 (pipe_nonblock(signal_pipe) != 0)
-       sudo_fatal(U_("unable to create pipe"));
+    if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1)
+       sudo_fatal(U_("unable to create sockets"));
 
-    /* Reset SIGWINCH and SIGALRM. */
+    /*
+     * Signals to forward to the child process (excluding SIGALRM).
+     * We block all other signals while running the signal handler.
+     * Note: HP-UX select() will not be interrupted if SA_RESTART set.
+     */
     memset(&sa, 0, sizeof(sa));
-    sigemptyset(&sa.sa_mask);
-    sa.sa_flags = SA_RESTART;
-    sa.sa_handler = SIG_DFL;
-    if (sudo_sigaction(SIGWINCH, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGWINCH);
-    if (sudo_sigaction(SIGALRM, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGALRM);
-
-    /* Ignore any SIGTTIN or SIGTTOU we get. */
-    sa.sa_handler = SIG_IGN;
-    if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN);
-    if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU);
-
-    /* Block all signals in mon_handler(). */
     sigfillset(&sa.sa_mask);
-
-    /* Note: HP-UX poll() will not be interrupted if SA_RESTART is set. */
-    sa.sa_flags = SA_INTERRUPT;
+    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
 #ifdef SA_SIGINFO
     sa.sa_flags |= SA_SIGINFO;
-    sa.sa_sigaction = mon_handler;
+    sa.sa_sigaction = exec_handler;
 #else
-    sa.sa_handler = mon_handler;
+    sa.sa_handler = exec_handler;
 #endif
+    if (sudo_sigaction(SIGTERM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTERM);
+    if (sudo_sigaction(SIGHUP, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGHUP);
+    if (sudo_sigaction(SIGALRM, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGALRM);
+    if (sudo_sigaction(SIGPIPE, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGPIPE);
+    if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR1);
+    if (sudo_sigaction(SIGUSR2, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR2);
     if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
-
-    /* Catch common signals so we can cleanup properly. */
-    sa.sa_flags = SA_RESTART;
-#ifdef SA_SIGINFO
-    sa.sa_flags |= SA_SIGINFO;
-    sa.sa_sigaction = mon_handler;
-#else
-    sa.sa_handler = mon_handler;
+#ifdef SIGINFO
+    if (sudo_sigaction(SIGINFO, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGINFO);
 #endif
-    if (sudo_sigaction(SIGHUP, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGHUP);
+
+    /*
+     * Unlike the non-pty case, we can use our normal signal handler
+     * for tty-generated signals triggered by the user.
+     */
     if (sudo_sigaction(SIGINT, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGINT);
     if (sudo_sigaction(SIGQUIT, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGQUIT);
-    if (sudo_sigaction(SIGTERM, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGTERM);
     if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
-    if (sudo_sigaction(SIGUSR1, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR1);
-    if (sudo_sigaction(SIGUSR2, &sa, NULL) != 0)
-       sudo_warn(U_("unable to set handler for signal %d"), SIGUSR2);
 
     /*
-     * Start a new session with the parent as the session leader
-     * and the slave pty as the controlling terminal.
-     * This allows us to be notified when the command has been suspended.
+     * We don't want to receive SIGTTIN/SIGTTOU, getting EIO is preferable.
      */
-    if (setsid() == -1) {
-       sudo_warn("setsid");
-       goto bad;
-    }
-    if (io_fds[SFD_SLAVE] != -1) {
-#ifdef TIOCSCTTY
-       if (ioctl(io_fds[SFD_SLAVE], TIOCSCTTY, NULL) != 0)
-           sudo_fatal(U_("unable to set controlling tty"));
-#else
-       /* Set controlling tty by reopening slave. */
-       if ((n = open(slavename, O_RDWR)) >= 0)
-           close(n);
-#endif
-    }
-
-    mon_pgrp = getpgrp();      /* save a copy of our process group */
+    sa.sa_flags = SA_RESTART;
+    sa.sa_handler = SIG_IGN;
+    if (sudo_sigaction(SIGTTIN, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTTIN);
+    if (sudo_sigaction(SIGTTOU, &sa, NULL) != 0)
+       sudo_warn(U_("unable to set handler for signal %d"), SIGTTOU);
 
     /*
-     * If stdin/stdout is not a tty, start command in the background
-     * since it might be part of a pipeline that reads from /dev/tty.
-     * In this case, we rely on the command receiving SIGTTOU or SIGTTIN
-     * when it needs access to the controlling tty.
+     * The policy plugin's session init must be run before we fork
+     * or certain pam modules won't be able to track their state.
      */
-    if (pipeline)
-       foreground = false;
-
-    /* Start command and wait for it to stop or exit */
-    if (pipe(errpipe) == -1)
-       sudo_fatal(U_("unable to create pipe"));
-    cmnd_pid = sudo_debug_fork();
-    if (cmnd_pid == -1) {
-       sudo_warn(U_("unable to fork"));
-       goto bad;
-    }
-    if (cmnd_pid == 0) {
-       /* We pass errno back to our parent via pipe on exec failure. */
-       close(backchannel);
-       close(signal_pipe[0]);
-       close(signal_pipe[1]);
-       close(errpipe[0]);
-       (void)fcntl(errpipe[1], F_SETFD, FD_CLOEXEC);
-       restore_signals();
+    if (policy_init_session(details) != true)
+       sudo_fatalx(U_("policy plugin failed session initialization"));
 
-       /* setup tty and exec command */
-       exec_pty(details, &cstat, errpipe[1]);
-       while (write(errpipe[1], &cstat, sizeof(cstat)) == -1) {
-           if (errno != EINTR)
-               break;
-       }
-       _exit(1);
-    }
-    close(errpipe[1]);
+    /*
+     * Child will run the command in the pty, parent will pass data
+     * to and from pty.
+     * XXX - inline fork_pty or refactor differently?
+     */
+    child = fork_pty(details, sv, &omask);
+    close(sv[1]);
 
     /* No longer need execfd. */
     if (details->execfd != -1) {
@@ -1454,128 +1256,179 @@ exec_monitor(struct command_details *details, int backchannel)
        details->execfd = -1;
     }
 
-    /* Send the command's pid to main sudo process. */
-    cstat.type = CMD_PID;
-    cstat.val = cmnd_pid;
-    ignore_result(send(backchannel, &cstat, sizeof(cstat), 0));
-
-    /* If any of stdin/stdout/stderr are pipes, close them in parent. */
-    if (io_fds[SFD_STDIN] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDIN]);
-    if (io_fds[SFD_STDOUT] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDOUT]);
-    if (io_fds[SFD_STDERR] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDERR]);
-
-    /* Put command in its own process group. */
-    cmnd_pgrp = cmnd_pid;
-    setpgid(cmnd_pid, cmnd_pgrp);
-
-    /* Make the command the foreground process for the pty slave. */
-    if (foreground && !ISSET(details->flags, CD_EXEC_BG)) {
-       do {
-           n = tcsetpgrp(io_fds[SFD_SLAVE], cmnd_pgrp);
-       } while (n == -1 && errno == EINTR);
-    }
+    /* Set command timeout if specified. */
+    if (ISSET(details->flags, CD_SET_TIMEOUT))
+       alarm(details->timeout);
 
     /*
-     * Create new event base and register read events for the
-     * signal pipe, error pipe, and backchannel.
+     * I/O logging must be in the C locale for floating point numbers
+     * to be logged consistently.
      */
-    evbase = sudo_ev_base_alloc();
-    if (evbase == NULL)
-       sudo_fatal(NULL);
+    setlocale(LC_ALL, "C");
 
-    memset(&cstat, 0, sizeof(cstat));
-    mc.cstat = &cstat;
-    mc.evbase = evbase;
-    mc.backchannel = backchannel;
+    /*
+     * Allocate event base and two persistent events:
+     * the signal pipe and the child process's backchannel.
+     */
+    fill_exec_closure_pty(&ec, cstat, details, child, &omask, sv[0]);
 
-    mc.signal_pipe_event = sudo_ev_alloc(signal_pipe[0],
-       SUDO_EV_READ|SUDO_EV_PERSIST, mon_signal_pipe_cb, &mc);
-    if (mc.signal_pipe_event == NULL)
-       sudo_fatal(NULL);
-    if (sudo_ev_add(evbase, mc.signal_pipe_event, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
+    /*
+     * In the event loop we pass input from user tty to master
+     * and pass output from master to stdout and IO plugin.
+     */
+    add_io_events(ec.evbase);
+    if (sudo_ev_loop(ec.evbase, 0) == -1)
+       sudo_warn(U_("error in event loop"));
+    if (sudo_ev_got_break(ec.evbase)) {
+       /* error from callback */
+       sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely");
+    }
 
-    mc.errpipe_event = sudo_ev_alloc(errpipe[0],
-       SUDO_EV_READ|SUDO_EV_PERSIST, mon_errpipe_cb, &mc);
-    if (mc.errpipe_event == NULL)
-       sudo_fatal(NULL);
-    if (sudo_ev_add(evbase, mc.errpipe_event, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
+    /* Flush any remaining output and free pty-related memory. */
+    pty_close(cstat);
 
-    mc.backchannel_event = sudo_ev_alloc(backchannel,
-       SUDO_EV_READ|SUDO_EV_PERSIST, mon_backchannel_cb, &mc);
-    if (mc.backchannel_event == NULL)
-       sudo_fatal(NULL);
-    if (sudo_ev_add(evbase, mc.backchannel_event, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
+    /* Free things up. */
+    sudo_ev_base_free(ec.evbase);
+    sudo_ev_free(ec.sigfwd_event);
+    sudo_ev_free(ec.signal_event);
+    sudo_ev_free(ec.backchannel_event);
+    TAILQ_FOREACH_SAFE(sigfwd, &ec.sigfwd_list, entries, sigfwd_next) {
+       free(sigfwd);
+    }
+    debug_return_int(cstat->type == CMD_ERRNO ? -1 : 0);
+}
+
+/*
+ * Schedule I/O events before starting the main event loop or
+ * resuming from suspend.
+ */
+static void
+add_io_events(struct sudo_event_base *evbase)
+{
+    struct io_buffer *iob;
+    debug_decl(add_io_events, SUDO_DEBUG_EXEC);
 
     /*
-     * Wait for errno on pipe, signal on backchannel or for SIGCHLD.
-     * The event loop ends when the child is no longer running and
-     * the error pipe is closed.
+     * Schedule all readers as long as the buffer is not full.
+     * Schedule writers that contain buffered data.
+     * Normally, write buffers are added on demand when data is read.
      */
-    (void) sudo_ev_loop(evbase, 0);
-    if (cmnd_pid != -1) {
-       /* XXX An error occurred, should send a message back. */
-       sudo_debug_printf(SUDO_DEBUG_ERROR,
-           "Command still running after event loop exit, sending SIGKILL");
-       kill(cmnd_pid, SIGKILL);
-    } else {
-       /* Send parent status. */
-       send_status(backchannel, &cstat);
+    SLIST_FOREACH(iob, &iobufs, entries) {
+       /* Don't read/write from /dev/tty if we are not in the foreground. */
+       if (iob->revent != NULL &&
+           (ttymode == TERM_RAW || !USERTTY_EVENT(iob->revent))) {
+           if (iob->len != sizeof(iob->buf)) {
+               sudo_debug_printf(SUDO_DEBUG_INFO,
+                   "added I/O revent %p, fd %d, events %d",
+                   iob->revent, iob->revent->fd, iob->revent->events);
+               if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1)
+                   sudo_fatal(U_("unable to add event to queue"));
+           }
+       }
+       if (iob->wevent != NULL &&
+           (foreground || !USERTTY_EVENT(iob->wevent))) {
+           if (iob->len > iob->off) {
+               sudo_debug_printf(SUDO_DEBUG_INFO,
+                   "added I/O wevent %p, fd %d, events %d",
+                   iob->wevent, iob->wevent->fd, iob->wevent->events);
+               if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
+                   sudo_fatal(U_("unable to add event to queue"));
+           }
+       }
     }
-    sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
-    _exit(1);
-
-bad:
-    debug_return_int(errno);
+    debug_return;
 }
 
 /*
- * Sets up std{in,out,err} and executes the actual command.
- * Returns only if execve() fails.
+ * Flush any output buffered in iobufs or readable from fds other
+ * than /dev/tty.  Removes I/O events from the event base when done.
  */
 static void
-exec_pty(struct command_details *details,
-    struct command_status *cstat, int errfd)
+del_io_events(bool nonblocking)
 {
-    volatile pid_t self = getpid();
-    debug_decl(exec_pty, SUDO_DEBUG_EXEC);
-
-    /* Register cleanup function */
-    sudo_fatal_callback_register(pty_cleanup);
-
-    /* Set command process group here too to avoid a race. */
-    setpgid(0, self);
-
-    /* Wire up standard fds, note that stdout/stderr may be pipes. */
-    if (dup2(io_fds[SFD_STDIN], STDIN_FILENO) == -1 ||
-       dup2(io_fds[SFD_STDOUT], STDOUT_FILENO) == -1 ||
-       dup2(io_fds[SFD_STDERR], STDERR_FILENO) == -1)
-       sudo_fatal("dup2");
-
-    /* Wait for parent to grant us the tty if we are foreground. */
-    if (foreground && !ISSET(details->flags, CD_EXEC_BG)) {
-       struct timespec ts = { 0, 1000 };  /* 1us */
-       while (tcgetpgrp(io_fds[SFD_SLAVE]) != self)
-           nanosleep(&ts, NULL);
+    struct io_buffer *iob;
+    struct sudo_event_base *evbase;
+    debug_decl(del_io_events, SUDO_DEBUG_EXEC);
+
+    /* Remove iobufs from existing event base. */
+    SLIST_FOREACH(iob, &iobufs, entries) {
+       if (iob->revent != NULL) {
+           sudo_debug_printf(SUDO_DEBUG_INFO,
+               "deleted I/O revent %p, fd %d, events %d",
+               iob->revent, iob->revent->fd, iob->revent->events);
+           sudo_ev_del(NULL, iob->revent);
+       }
+       if (iob->wevent != NULL) {
+           sudo_debug_printf(SUDO_DEBUG_INFO,
+               "deleted I/O wevent %p, fd %d, events %d",
+               iob->wevent, iob->wevent->fd, iob->wevent->events);
+           sudo_ev_del(NULL, iob->wevent);
+       }
+    }
+
+    /* Create temporary event base for flushing. */
+    evbase = sudo_ev_base_alloc();
+    if (evbase == NULL)
+       sudo_fatal(NULL);
+
+    /* Avoid reading from /dev/tty, just flush existing data. */
+    SLIST_FOREACH(iob, &iobufs, entries) {
+       /* Don't read from /dev/tty while flushing. */
+       if (iob->revent != NULL && !USERTTY_EVENT(iob->revent)) {
+           if (iob->len != sizeof(iob->buf)) {
+               if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1)
+                   sudo_fatal(U_("unable to add event to queue"));
+           }
+       }
+       /* Flush any write buffers with data in them. */
+       if (iob->wevent != NULL) {
+           if (iob->len > iob->off) {
+               if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
+                   sudo_fatal(U_("unable to add event to queue"));
+           }
+       }
+    }
+    (void) sudo_ev_loop(evbase, SUDO_EVLOOP_NONBLOCK);
+
+    /*
+     * If not in non-blocking mode, make sure we flush write buffers.
+     * We don't want to read from the pty or stdin since that might block
+     * and the command is no longer running anyway.
+     */
+    if (!nonblocking) {
+       /* Clear out iobufs from event base. */
+       SLIST_FOREACH(iob, &iobufs, entries) {
+           if (iob->revent != NULL && !USERTTY_EVENT(iob->revent))
+               sudo_ev_del(evbase, iob->revent);
+           if (iob->wevent != NULL)
+               sudo_ev_del(evbase, iob->wevent);
+       }
+
+       SLIST_FOREACH(iob, &iobufs, entries) {
+           /* Flush any write buffers with data in them. */
+           if (iob->wevent != NULL) {
+               if (iob->len > iob->off) {
+                   if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
+                       sudo_fatal(U_("unable to add event to queue"));
+               }
+           }
+       }
+       (void) sudo_ev_loop(evbase, 0);
+     
+       /* We should now have flushed all write buffers. */
+       SLIST_FOREACH(iob, &iobufs, entries) {
+           if (iob->wevent != NULL) {
+               if (iob->len > iob->off) {
+                   sudo_debug_printf(SUDO_DEBUG_ERROR,
+                       "unflushed data: wevent %p, fd %d, events %d",
+                       iob->wevent, iob->wevent->fd, iob->wevent->events);
+               }
+           }
+       }
     }
 
-    /* We have guaranteed that the slave fd is > 2 */
-    if (io_fds[SFD_SLAVE] != -1)
-       close(io_fds[SFD_SLAVE]);
-    if (io_fds[SFD_STDIN] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDIN]);
-    if (io_fds[SFD_STDOUT] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDOUT]);
-    if (io_fds[SFD_STDERR] != io_fds[SFD_SLAVE])
-       close(io_fds[SFD_STDERR]);
-
-    /* Execute command; only returns on error. */
-    exec_cmnd(details, cstat, errfd);
+    /* Free temporary event base, removing its events. */
+    sudo_ev_base_free(evbase);
 
     debug_return;
 }
index 6dc795cbc306eb198330e6b225ce617da1446633..302bd42c155c7680ea4df51e6f14a9d3fe47b1ba 100644 (file)
 # endif
 #endif
 
+/*
+ * Indices into io_fds[] when running a command in a pty.
+ */
+#define SFD_STDIN      0
+#define SFD_STDOUT     1
+#define SFD_STDERR     2
+#define SFD_MASTER     3
+#define SFD_SLAVE      4
+#define SFD_USERTTY    5
+
 /*
  * Special values to indicate whether continuing in foreground or background.
  */
 #define SESH_ERR_SOME_FILES 33         /* copy error, some files copied */
 
 /*
- * Symbols shared between exec.c and exec_pty.c
+ * Symbols shared between exec.c, exec_nopty.c, exec_pty.c and exec_monitor.c
  */
+struct command_details;
+struct command_status;
 
 /* exec.c */
-extern volatile pid_t cmnd_pid;
+extern volatile pid_t cmnd_pid, ppgrp;
+void exec_cmnd(struct command_details *details, int errfd);
+void terminate_command(pid_t pid, bool use_pgrp);
+#ifdef SA_SIGINFO
+void exec_handler(int s, siginfo_t *info, void *context);
+#else
+void exec_handler(int s);
+#endif
 
 /* exec_common.c */
 int sudo_execve(int fd, const char *path, char *const argv[], char *envp[], bool noexec);
 char **disable_execute(char *envp[], const char *dso);
 
+/* exec_nopty.c */
+int exec_nopty(struct command_details *details, struct command_status *cstat);
+
 /* exec_pty.c */
-struct sudo_event_base;
-struct command_details;
-struct command_status;
-int fork_pty(struct command_details *details, int sv[], sigset_t *omask);
-int suspend_parent(int signo);
-void exec_cmnd(struct command_details *details, struct command_status *cstat,
-    int errfd);
-void add_io_events(struct sudo_event_base *evbase);
-#ifdef SA_SIGINFO
-void handler(int s, siginfo_t *info, void *context);
-#else
-void handler(int s);
-#endif
-void pty_close(struct command_status *cstat);
-void pty_setup(uid_t uid, const char *tty, const char *utmp_user);
-void terminate_command(pid_t pid, bool use_pgrp);
+int exec_pty(struct command_details *details, struct command_status *cstat);
+void pty_cleanup(void);
+int pty_make_controlling(void);
+
+/* exec_monitor.c */
+int exec_monitor(struct command_details *details, bool foreground, int backchannel);
 
 /* utmp.c */
 bool utmp_login(const char *from_line, const char *to_line, int ttyfd,