From 6dba84dca9e53ea5fc12c59faab6bdf3bbab8f51 Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Fri, 3 Mar 2017 10:35:11 -0700 Subject: [PATCH] Reorganize the command execution code to separate out the pty and 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 | 2 + src/Makefile.in | 28 +- src/exec.c | 939 +++++---------------------------- src/exec_monitor.c | 664 ++++++++++++++++++++++++ src/exec_nopty.c | 505 ++++++++++++++++++ src/exec_pty.c | 1233 +++++++++++++++++++------------------------- src/sudo_exec.h | 48 +- 7 files changed, 1894 insertions(+), 1525 deletions(-) create mode 100644 src/exec_monitor.c create mode 100644 src/exec_nopty.c diff --git a/MANIFEST b/MANIFEST index ec7363afb..6d487c73a 100644 --- 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 diff --git a/src/Makefile.in b/src/Makefile.in index 16449a8b4..b7f13d8c7 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -1,5 +1,5 @@ # -# Copyright (c) 2010-2015 Todd C. Miller +# Copyright (c) 2010-2017 Todd C. Miller # # 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 \ diff --git a/src/exec.c b/src/exec.c index 9d24da5a6..3bc023158 100644 --- a/src/exec.c +++ b/src/exec.c @@ -17,11 +17,6 @@ #include #include -#include -#include -#include -#include -#include #include #include #ifdef HAVE_STRING_H @@ -31,13 +26,9 @@ # include #endif /* HAVE_STRINGS_H */ #include -#ifdef TIME_WITH_SYS_TIME -# include -#endif #include #include #include -#include #include "sudo.h" #include "sudo_exec.h" @@ -45,102 +36,60 @@ #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 index 000000000..b447f47fa --- /dev/null +++ b/src/exec_monitor.c @@ -0,0 +1,664 @@ +/* + * Copyright (c) 2009-2017 Todd C. Miller + * + * 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 + +#include +#include +#include +#include +#include +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include +#endif /* HAVE_STRINGS_H */ +#include +#include +#include +#include +#include + +#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 index 000000000..c72eb3159 --- /dev/null +++ b/src/exec_nopty.c @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2009-2017 Todd C. Miller + * + * 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 + +#include +#include +#include +#include +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include +#endif /* HAVE_STRINGS_H */ +#include +#include +#include +#include + +#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 */ diff --git a/src/exec_pty.c b/src/exec_pty.c index 885b65477..796aa7fef 100644 --- a/src/exec_pty.c +++ b/src/exec_pty.c @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -30,13 +29,9 @@ # include #endif /* HAVE_STRINGS_H */ #include -#ifdef TIME_WITH_SYS_TIME -# include -#endif #include #include #include -#include #include "sudo.h" #include "sudo_event.h" @@ -44,19 +39,31 @@ #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; } diff --git a/src/sudo_exec.h b/src/sudo_exec.h index 6dc795cbc..302bd42c1 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -35,6 +35,16 @@ # 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. */ @@ -69,33 +79,35 @@ #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, -- 2.40.0