/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2009-2023 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * 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.
 */

/*
 * This is an open source non-commercial project. Dear PVS-Studio, please check it.
 * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
 */

#include <config.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>

#include "sudo.h"
#include "sudo_exec.h"
#include "sudo_plugin.h"
#include "sudo_plugin_int.h"

struct monitor_closure {
    const struct command_details *details;
    struct sudo_event_base *evbase;
    struct sudo_event *errpipe_event;
    struct sudo_event *backchannel_event;
    struct sudo_event *sigint_event;
    struct sudo_event *sigquit_event;
    struct sudo_event *sigtstp_event;
    struct sudo_event *sigterm_event;
    struct sudo_event *sighup_event;
    struct sudo_event *sigusr1_event;
    struct sudo_event *sigusr2_event;
    struct sudo_event *sigchld_event;
    struct command_status *cstat;
    pid_t cmnd_pid;
    pid_t cmnd_pgrp;
    pid_t mon_pgrp;
    int backchannel;
};

/*
 * 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(struct monitor_closure *mc, int signo, bool from_parent)
{
    debug_decl(deliver_signal, SUDO_DEBUG_EXEC);

    /* Avoid killing more than a single process or process group. */
    if (mc->cmnd_pid <= 0)
	debug_return;

    if (sudo_debug_needed(SUDO_DEBUG_INFO)) {
	char signame[SIG2STR_MAX];
	if (signo == SIGCONT_FG)
	    (void)strlcpy(signame, "CONT_FG", sizeof(signame));
	else if (signo == SIGCONT_BG)
	    (void)strlcpy(signame, "CONT_BG", sizeof(signame));
	else if (sig2str(signo, signame) == -1)
	    (void)snprintf(signame, sizeof(signame), "%d", signo);
	sudo_debug_printf(SUDO_DEBUG_INFO, "received SIG%s%s",
	    signame, from_parent ? " from parent" : "");
    }

    /* Handle signal from parent or monitor. */
    switch (signo) {
    case SIGALRM:
	terminate_command(mc->cmnd_pid, true);
	break;
    case SIGCONT_FG:
	/* Continue in foreground, grant it controlling tty. */
	if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc->cmnd_pgrp) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		"%s: unable to set foreground pgrp to %d (command)",
		__func__, (int)mc->cmnd_pgrp);
	}
	killpg(mc->cmnd_pid, SIGCONT);
	break;
    case SIGCONT_BG:
	/* Continue in background, I take controlling tty. */
	if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc->mon_pgrp) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		"%s: unable to set foreground pgrp to %d (monitor)",
		__func__, (int)mc->mon_pgrp);
	}
	killpg(mc->cmnd_pid, SIGCONT);
	break;
    case SIGKILL:
	_exit(EXIT_FAILURE); /* XXX */
	/* NOTREACHED */
    default:
	/* Relay signal to command. */
	killpg(mc->cmnd_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);
	n = send(fd, cstat, sizeof(*cstat), 0);
	if (n != sizeof(*cstat)) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		"%s: unable to send status to parent", __func__);
	}
	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
mon_handle_sigchld(struct monitor_closure *mc)
{
    char signame[SIG2STR_MAX];
    int status;
    pid_t pid;
    debug_decl(mon_handle_sigchld, SUDO_DEBUG_EXEC);

    /* Read command status. */
    do {
	pid = waitpid(mc->cmnd_pid, &status, WUNTRACED|WNOHANG);
    } while (pid == -1 && errno == EINTR);
    switch (pid) {
    case -1:
	if (errno != ECHILD) {
	    sudo_warn(U_("%s: %s"), __func__, "waitpid");
	    debug_return;
	}
	FALLTHROUGH;
    case 0:
	/* Nothing to wait for. */
	sudo_debug_printf(SUDO_DEBUG_INFO, "%s: no process to wait for",
	    __func__);
	debug_return;
    }

    if (WIFSTOPPED(status)) {
	if (sig2str(WSTOPSIG(status), signame) == -1)
	    (void)snprintf(signame, sizeof(signame), "%d", WSTOPSIG(status));
	sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) stopped, SIG%s",
	    __func__, (int)mc->cmnd_pid, signame);
    } else if (WIFSIGNALED(status)) {
	if (sig2str(WTERMSIG(status), signame) == -1)
	    (void)snprintf(signame, sizeof(signame), "%d", WTERMSIG(status));
	sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) killed, SIG%s",
	    __func__, (int)mc->cmnd_pid, signame);
	mc->cmnd_pid = -1;
    } else if (WIFEXITED(status)) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "%s: command (%d) exited: %d",
	    __func__, (int)mc->cmnd_pid, WEXITSTATUS(status));
	mc->cmnd_pid = -1;
    } else {
	sudo_debug_printf(SUDO_DEBUG_WARN,
	    "%s: unexpected wait status 0x%x for command (%d)",
	    __func__, status, (int)mc->cmnd_pid);
    }

    /* Don't overwrite execve() failure with child exit status. */
    if (mc->cstat->type == CMD_INVALID) {
	/*
	 * Store wait status in cstat and forward to parent if stopped.
	 */
	mc->cstat->type = CMD_WSTATUS;
	mc->cstat->val = status;
	if (WIFSTOPPED(status)) {
	    /* Save the foreground pgid so we can restore it later. */
	    pid = tcgetpgrp(io_fds[SFD_FOLLOWER]);
	    if (pid != mc->mon_pgrp)
		mc->cmnd_pgrp = pid;
	    send_status(mc->backchannel, mc->cstat);
	}
    } else {
	sudo_debug_printf(SUDO_DEBUG_WARN,
	    "%s: not overwriting command status %d,%d with %d,%d",
	    __func__, mc->cstat->type, mc->cstat->val, CMD_WSTATUS, status);
    }

    debug_return;
}

static void
mon_signal_cb(int signo, int what, void *v)
{
    struct sudo_ev_siginfo_container *sc = v;
    struct monitor_closure *mc = sc->closure;
    debug_decl(mon_signal_cb, SUDO_DEBUG_EXEC);

    /*
     * Handle SIGCHLD specially and deliver other signals
     * directly to the command.
     */
    if (signo == SIGCHLD) {
	mon_handle_sigchld(mc);
	if (mc->cmnd_pid == -1) {
	    /* Command exited or was killed, exit event loop. */
	    sudo_ev_loopexit(mc->evbase);
	}
    } else {
	/*
	 * 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 (USER_SIGNALED(sc->siginfo) && sc->siginfo->si_pid != 0) {
	    pid_t si_pgrp;

	    if (sc->siginfo->si_pid == mc->cmnd_pid)
		    debug_return;
	    si_pgrp = getpgid(sc->siginfo->si_pid);
	    if (si_pgrp != -1) {
		if (si_pgrp == mc->cmnd_pgrp)
		    debug_return;
	    }
	}
	deliver_signal(mc, 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 nread;
    int errval;
    debug_decl(mon_errpipe_cb, SUDO_DEBUG_EXEC);

    /*
     * Read errno from child or EOF when command is executed.
     * Note that the error pipe is *blocking*.
     */
    nread = read(fd, &errval, sizeof(errval));
    switch (nread) {
    case -1:
	if (errno != EAGAIN && errno != EINTR) {
	    if (mc->cstat->val == CMD_INVALID) {
		/* XXX - need a way to distinguish non-exec error. */
		mc->cstat->type = CMD_ERRNO;
		mc->cstat->val = errno;
	    }
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		"%s: failed to read error pipe", __func__);
	    sudo_ev_loopbreak(mc->evbase);
	}
	break;
    default:
	if (nread == 0) {
	    /* The error pipe closes when the command is executed. */
	    sudo_debug_printf(SUDO_DEBUG_INFO, "EOF on error pipe");
	} else {
	    /* 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);
	close(fd);
	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.
     * Note that the backchannel is a *blocking* socket.
     */
    n = recv(fd, &cstmp, sizeof(cstmp), MSG_WAITALL);
    if (n != sizeof(cstmp)) {
	if (n == -1) {
	    if (errno == EINTR || errno == EAGAIN)
		debug_return;
	    sudo_warn("%s", U_("error reading from socketpair"));
	} else {
	    /* short read or EOF, parent process died? */
	}
	/* XXX - need a way to distinguish non-exec error. */
	mc->cstat->type = CMD_ERRNO;
	mc->cstat->val = n ? EIO : ECONNRESET;
	sudo_ev_loopbreak(mc->evbase);
    } else {
	if (cstmp.type == CMD_SIGNO) {
	    deliver_signal(mc, 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, sigset_t *mask,
    bool foreground, int intercept_fd, int errfd)
{
    volatile pid_t self = getpid();
    debug_decl(exec_cmnd_pty, SUDO_DEBUG_EXEC);

    /* 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 (dup3(io_fds[SFD_STDIN], STDIN_FILENO, 0) == -1)
	sudo_fatal("dup3");
    if (io_fds[SFD_STDIN] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDIN]);
    if (dup3(io_fds[SFD_STDOUT], STDOUT_FILENO, 0) == -1)
	sudo_fatal("dup3");
    if (io_fds[SFD_STDOUT] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDOUT]);
    if (dup3(io_fds[SFD_STDERR], STDERR_FILENO, 0) == -1)
	sudo_fatal("dup3");
    if (io_fds[SFD_STDERR] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDERR]);

    /* 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 */
	sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: waiting for controlling tty",
	    __func__);
	while (tcgetpgrp(io_fds[SFD_FOLLOWER]) != self)
	    nanosleep(&ts, NULL);
	sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: got controlling tty",
	    __func__);
    }

    /* Done with the pty follower, don't leak it. */
    if (io_fds[SFD_FOLLOWER] != -1)
	close(io_fds[SFD_FOLLOWER]);

    /* Execute command; only returns on error. */
    sudo_debug_printf(SUDO_DEBUG_INFO, "executing %s in the %s",
	details->command, foreground ? "foreground" : "background");
    exec_cmnd(details, mask, intercept_fd, 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,
    const struct command_details *details, 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. */
    mc->details = details;
    mc->cstat = cstat;
    mc->backchannel = backchannel;
    mc->mon_pgrp = getpgrp();

    /* Setup event base and events. */
    mc->evbase = sudo_ev_base_alloc();
    if (mc->evbase == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));

    /* 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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->errpipe_event, NULL, false) == -1)
	sudo_fatal("%s", 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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->backchannel_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    /* Events for local signals. */
    mc->sigint_event = sudo_ev_alloc(SIGINT,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigint_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigint_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigquit_event = sudo_ev_alloc(SIGQUIT,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigquit_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigquit_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigtstp_event = sudo_ev_alloc(SIGTSTP,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigtstp_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigtstp_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigterm_event = sudo_ev_alloc(SIGTERM,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigterm_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigterm_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sighup_event = sudo_ev_alloc(SIGHUP,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sighup_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sighup_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigusr1_event = sudo_ev_alloc(SIGUSR1,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigusr1_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigusr1_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigusr2_event = sudo_ev_alloc(SIGUSR2,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigusr2_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigusr2_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    mc->sigchld_event = sudo_ev_alloc(SIGCHLD,
	SUDO_EV_SIGINFO, mon_signal_cb, mc);
    if (mc->sigchld_event == NULL)
	sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    if (sudo_ev_add(mc->evbase, mc->sigchld_event, NULL, false) == -1)
	sudo_fatal("%s", U_("unable to add event to queue"));

    /* Clear the default event base. */
    sudo_ev_base_setdef(NULL);

    debug_return;
}

/*
 * Make the tty follower the controlling tty.
 */
static bool
pty_make_controlling(const char *follower)
{
    debug_decl(pty_make_controlling, SUDO_DEBUG_EXEC);

    if (io_fds[SFD_FOLLOWER] != -1) {
#ifdef TIOCSCTTY
	if (ioctl(io_fds[SFD_FOLLOWER], TIOCSCTTY, NULL) != 0)
	    debug_return_bool(false);
#else
	/* Set controlling tty by reopening pty follower. */
	int fd = open(follower, O_RDWR);
	if (fd == -1)
	    debug_return_bool(false);
	close(fd);
#endif
    }
    debug_return_bool(true);
}

/*
 * 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.
 * Must be called with signals blocked and the old signal mask in oset.
 * Returns an error if fork(2) fails, else calls _exit(2).
 */
int
exec_monitor(struct command_details *details, sigset_t *oset,
    bool foreground, int backchannel, int intercept_fd)
{
    struct monitor_closure mc = { 0 };
    struct command_status cstat;
    struct sigaction sa;
    int errpipe[2];
    debug_decl(exec_monitor, SUDO_DEBUG_EXEC);

    /* Close fds the monitor doesn't use. */
    if (io_fds[SFD_LEADER] != -1)
	close(io_fds[SFD_LEADER]);
    if (io_fds[SFD_USERTTY] != -1)
	close(io_fds[SFD_USERTTY]);

    /* Ignore any SIGTTIN or SIGTTOU we receive (shouldn't be possible). */
    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    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);

    /*
     * Start a new session with the parent as the session leader
     * and the follower device 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(details->tty)) {
	sudo_warn("%s", U_("unable to set controlling tty"));
	goto bad;
    }

    /*
     * We use a pipe to get errno if execve(2) fails in the child.
     */
    if (pipe2(errpipe, O_CLOEXEC) != 0) {
	sudo_warn("%s", U_("unable to create pipe"));
	goto bad;
    }

    /*
     * Before forking, wait for the main sudo process to tell us to go.
     * Avoids race conditions when the command exits quickly.
     */
    while (recv(backchannel, &cstat, sizeof(cstat), MSG_WAITALL) == -1) {
	if (errno != EINTR && errno != EAGAIN) {
	    sudo_warn("%s", U_("unable to receive message from parent"));
	    goto bad;
	}
    }

#ifdef HAVE_SELINUX
    if (ISSET(details->flags, CD_RBAC_ENABLED)) {
        if (selinux_relabel_tty(details->tty, io_fds[SFD_FOLLOWER]) == -1)
            goto bad;
	selinux_audit_role_change();
    }
#endif

    mc.cmnd_pid = sudo_debug_fork();
    switch (mc.cmnd_pid) {
    case -1:
	sudo_warn("%s", U_("unable to fork"));
#ifdef HAVE_SELINUX
	if (ISSET(details->flags, CD_RBAC_ENABLED)) {
	    if (selinux_restore_tty() != 0)
		sudo_warnx("%s", U_("unable to restore tty label"));
	}
#endif
	goto bad;
    case 0:
	/* child */
	close(backchannel);
	close(errpipe[0]);
	/* setup tty and exec command */
	exec_cmnd_pty(details, oset, foreground, intercept_fd, errpipe[1]);
	if (write(errpipe[1], &errno, sizeof(int)) == -1)
	    sudo_warn(U_("unable to execute %s"), details->command);
	_exit(EXIT_FAILURE);
    }
    close(errpipe[1]);
    if (intercept_fd != -1)
	close(intercept_fd);

    /* 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 = mc.cmnd_pid;
    send_status(backchannel, &cstat);

    /*
     * Create new event base and register read events for the
     * signal pipe, error pipe, and backchannel.
     */
    fill_exec_closure_monitor(&mc, details, &cstat, errpipe[0], backchannel);

    /* Restore signal mask now that signal handlers are setup. */
    sigprocmask(SIG_SETMASK, oset, NULL);

    /* If any of stdin/stdout/stderr are pipes, close them in parent. */
    if (io_fds[SFD_STDIN] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDIN]);
    if (io_fds[SFD_STDOUT] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDOUT]);
    if (io_fds[SFD_STDERR] != io_fds[SFD_FOLLOWER])
	close(io_fds[SFD_STDERR]);

    /* Put command in its own process group. */
    mc.cmnd_pgrp = mc.cmnd_pid;
    setpgid(mc.cmnd_pid, mc.cmnd_pgrp);

    /* Make the command the foreground process for the pty follower. */
    if (foreground && !ISSET(details->flags, CD_EXEC_BG)) {
	if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc.cmnd_pgrp) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		"%s: unable to set foreground pgrp to %d (command)",
		__func__, (int)mc.cmnd_pgrp);
	}
    }

    /*
     * 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.
     */
    cstat.type = CMD_INVALID;
    cstat.val = 0;
    (void) sudo_ev_dispatch(mc.evbase);
    if (mc.cmnd_pid != -1) {
	pid_t pid;

	/* Command still running, did the parent die? */
	sudo_debug_printf(SUDO_DEBUG_ERROR,
	    "Command still running after event loop exit, terminating");
	terminate_command(mc.cmnd_pid, true);
	do {
	    pid = waitpid(mc.cmnd_pid, NULL, 0);
	} while (pid == -1 && errno == EINTR);
	/* XXX - update cstat with wait status? */
    }

    /*
     * Take the controlling tty.  This prevents processes spawned by the
     * command from receiving SIGHUP when the session leader (us) exits.
     */
    if (tcsetpgrp(io_fds[SFD_FOLLOWER], mc.mon_pgrp) == -1) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
	    "%s: unable to set foreground pgrp to %d (monitor)",
	    __func__, (int)mc.mon_pgrp);
    }

    /* Send parent status. */
    send_status(backchannel, &cstat);

#ifdef HAVE_SELINUX
    if (ISSET(details->flags, CD_RBAC_ENABLED)) {
	if (selinux_restore_tty() != 0)
	    sudo_warnx("%s", U_("unable to restore tty label"));
    }
#endif
    sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
    _exit(EXIT_FAILURE);

bad:
    debug_return_int(-1);
}
