Add support for logging stdin/stdout/stderr in the non-pty exec path.

If we are logging I/O but not terminal input/output (either because
no terminal is present or because that is what the plugin requested),
the non-pty exec path is now taken.
This commit is contained in:
Todd C. Miller
2022-09-27 13:46:55 -06:00
parent 803b4939be
commit 87b7209ebb
8 changed files with 292 additions and 37 deletions

View File

@@ -25,7 +25,7 @@
.nr BA @BAMAN@
.nr LC @LCMAN@
.nr PS @PSMAN@
.TH "SUDOERS" "@mansectform@" "September 20, 2022" "Sudo @PACKAGE_VERSION@" "File Formats Manual"
.TH "SUDOERS" "@mansectform@" "September 27, 2022" "Sudo @PACKAGE_VERSION@" "File Formats Manual"
.nh
.if n .ad l
.SH "NAME"
@@ -3135,8 +3135,7 @@ This setting is only supported by version 1.9.0 or higher.
log_stderr
If set,
\fBsudo\fR
will run the command in a pseudo-terminal and log the standard error
if it is not connected to the user's terminal.
will log the standard error if it is not connected to the user's terminal.
This can be used to log output to a pipe or redirected to a file.
This flag is
\fIoff\fR
@@ -3149,8 +3148,7 @@ command tag is set.
log_stdin
If set,
\fBsudo\fR
will run the command in a pseudo-terminal and log the standard input
if it is not connected to the user's terminal.
will log the standard input if it is not connected to the user's terminal.
This can be used to log input from a pipe or redirected from a file.
This flag is
\fIoff\fR
@@ -3163,8 +3161,7 @@ command tag is set.
log_stdout
If set,
\fBsudo\fR
will run the command in a pseudo-terminal and log the standard output
if it is not connected to the user's terminal.
will log the standard output if it is not connected to the user's terminal.
This can be used to log output to a pipe or redirected to a file.
This flag is
\fIoff\fR

View File

@@ -25,7 +25,7 @@
.nr BA @BAMAN@
.nr LC @LCMAN@
.nr PS @PSMAN@
.Dd September 20, 2022
.Dd September 27, 2022
.Dt SUDOERS @mansectform@
.Os Sudo @PACKAGE_VERSION@
.Sh NAME
@@ -2963,8 +2963,7 @@ This setting is only supported by version 1.9.0 or higher.
.It log_stderr
If set,
.Nm sudo
will run the command in a pseudo-terminal and log the standard error
if it is not connected to the user's terminal.
will log the standard error if it is not connected to the user's terminal.
This can be used to log output to a pipe or redirected to a file.
This flag is
.Em off
@@ -2976,8 +2975,7 @@ command tag is set.
.It log_stdin
If set,
.Nm sudo
will run the command in a pseudo-terminal and log the standard input
if it is not connected to the user's terminal.
will log the standard input if it is not connected to the user's terminal.
This can be used to log input from a pipe or redirected from a file.
This flag is
.Em off
@@ -2989,8 +2987,7 @@ command tag is set.
.It log_stdout
If set,
.Nm sudo
will run the command in a pseudo-terminal and log the standard output
if it is not connected to the user's terminal.
will log the standard output if it is not connected to the user's terminal.
This can be used to log output to a pipe or redirected to a file.
This flag is
.Em off

View File

@@ -360,9 +360,6 @@ sudo_terminated(struct command_status *cstat)
debug_return_bool(false);
}
#if SUDO_API_VERSION != SUDO_API_MKVERSION(1, 20)
# error "Update sudo_needs_pty() after changing the plugin API"
#endif
static bool
sudo_needs_pty(struct command_details *details)
{
@@ -373,12 +370,7 @@ sudo_needs_pty(struct command_details *details)
TAILQ_FOREACH(plugin, &io_plugins, entries) {
if (plugin->u.io->log_ttyin != NULL ||
plugin->u.io->log_ttyout != NULL ||
plugin->u.io->log_stdin != NULL ||
plugin->u.io->log_stdout != NULL ||
plugin->u.io->log_stderr != NULL ||
plugin->u.io->change_winsize != NULL ||
plugin->u.io->log_suspend != NULL)
plugin->u.io->log_ttyout != NULL)
return true;
}
return false;

View File

@@ -23,8 +23,10 @@
#include <config.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#if defined(HAVE_STDINT_H)
# include <stdint.h>
@@ -46,6 +48,27 @@
static void handle_sigchld_nopty(struct exec_closure *ec);
/*
* Handle window size change events.
*/
static void
handle_sigwinch(struct exec_closure *ec, int fd)
{
struct winsize wsize;
debug_decl(handle_sigwinch, SUDO_DEBUG_EXEC);
if (fd != -1 && ioctl(fd, TIOCGWINSZ, &wsize) == 0) {
if (wsize.ws_row != ec->rows || wsize.ws_col != ec->cols) {
/* Log window change event. */
log_winchange(ec, wsize.ws_row, wsize.ws_col);
/* Update rows/cols. */
ec->rows = wsize.ws_row;
ec->cols = wsize.ws_col;
}
}
}
/* Note: this is basically the same as mon_errpipe_cb() in exec_monitor.c */
static void
errpipe_cb(int fd, int what, void *v)
@@ -117,6 +140,9 @@ signal_cb_nopty(int signo, int what, void *v)
sudo_ev_loopexit(ec->evbase);
}
debug_return;
case SIGWINCH:
handle_sigwinch(ec, io_fds[SFD_USERTTY]);
FALLTHROUGH;
#ifdef SIGINFO
case SIGINFO:
#endif
@@ -186,6 +212,8 @@ fill_exec_closure(struct exec_closure *ec,
ec->ppgrp = getpgrp();
ec->cstat = cstat;
ec->details = details;
ec->rows = user_details.ts_rows;
ec->cols = user_details.ts_cols;
/* Setup event base and events. */
ec->evbase = details->evbase;
@@ -287,22 +315,222 @@ fill_exec_closure(struct exec_closure *ec,
sudo_fatal("%s", U_("unable to add event to queue"));
#endif
ec->sigwinch_event = sudo_ev_alloc(SIGWINCH,
SUDO_EV_SIGINFO, signal_cb_nopty, ec);
if (ec->sigwinch_event == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
if (sudo_ev_add(ec->evbase, ec->sigwinch_event, NULL, false) == -1)
sudo_fatal("%s", U_("unable to add event to queue"));
/* Set the default event base. */
sudo_ev_base_setdef(ec->evbase);
debug_return;
}
/*
* Read an iobuf that is ready.
*/
static void
read_callback(int fd, int what, void *v)
{
struct io_buffer *iob = v;
struct sudo_event_base *evbase = sudo_ev_get_base(iob->revent);
ssize_t n;
debug_decl(read_callback, SUDO_DEBUG_EXEC);
n = read(fd, iob->buf + iob->len, sizeof(iob->buf) - iob->len);
switch (n) {
case -1:
if (errno == EAGAIN || errno == EINTR) {
/* Not an error, retry later. */
break;
}
/* Treat read error as fatal and close the fd. */
sudo_debug_printf(SUDO_DEBUG_ERROR,
"error reading fd %d: %s", fd, strerror(errno));
FALLTHROUGH;
case 0:
/* got EOF */
if (n == 0) {
sudo_debug_printf(SUDO_DEBUG_INFO,
"read EOF from fd %d", fd);
}
safe_close(fd);
ev_free_by_fd(evbase, fd);
/* If writer already consumed the buffer, close it too. */
if (iob->wevent != NULL && iob->off == iob->len) {
safe_close(sudo_ev_get_fd(iob->wevent));
ev_free_by_fd(evbase, sudo_ev_get_fd(iob->wevent));
iob->off = iob->len = 0;
}
break;
default:
sudo_debug_printf(SUDO_DEBUG_INFO,
"read %zd bytes from fd %d", n, fd);
if (!iob->action(iob->buf + iob->len, n, iob)) {
terminate_command(iob->ec->cmnd_pid, true);
iob->ec->cmnd_pid = -1;
}
iob->len += n;
/* Disable reader if buffer is full. */
if (iob->len == sizeof(iob->buf))
sudo_ev_del(evbase, iob->revent);
/* Enable writer now that there is new data in the buffer. */
if (iob->wevent != NULL) {
if (sudo_ev_add(evbase, iob->wevent, NULL, false) == -1)
sudo_fatal("%s", U_("unable to add event to queue"));
}
break;
}
debug_return;
}
/*
* Write an iobuf that is ready.
*/
static void
write_callback(int fd, int what, void *v)
{
struct io_buffer *iob = v;
struct sudo_event_base *evbase = sudo_ev_get_base(iob->wevent);
ssize_t n;
debug_decl(write_callback, SUDO_DEBUG_EXEC);
n = write(fd, iob->buf + iob->off, iob->len - iob->off);
if (n == -1) {
switch (errno) {
case EPIPE:
case EBADF:
/* other end of pipe closed */
sudo_debug_printf(SUDO_DEBUG_INFO,
"unable to write %d bytes to fd %d",
iob->len - iob->off, fd);
/* Close reader if there is one. */
if (iob->revent != NULL) {
safe_close(sudo_ev_get_fd(iob->revent));
ev_free_by_fd(evbase, sudo_ev_get_fd(iob->revent));
}
safe_close(fd);
ev_free_by_fd(evbase, fd);
break;
case EINTR:
case EAGAIN:
/* Not an error, retry later. */
break;
default:
/* XXX - need a way to distinguish non-exec error. */
iob->ec->cstat->type = CMD_ERRNO;
iob->ec->cstat->val = errno;
sudo_debug_printf(SUDO_DEBUG_ERROR,
"error writing fd %d: %s", fd, strerror(errno));
sudo_ev_loopbreak(evbase);
break;
}
} else {
sudo_debug_printf(SUDO_DEBUG_INFO,
"wrote %zd bytes to fd %d", n, fd);
iob->off += n;
/* Disable writer and reset the buffer if fully consumed. */
if (iob->off == iob->len) {
iob->off = iob->len = 0;
sudo_ev_del(evbase, iob->wevent);
/* Forward the EOF from reader to writer. */
if (iob->revent == NULL) {
safe_close(fd);
ev_free_by_fd(evbase, fd);
}
}
/* Enable reader if buffer is not full. */
if (iob->revent != NULL && iob->len != sizeof(iob->buf)) {
if (sudo_ev_add(evbase, iob->revent, NULL, false) == -1)
sudo_fatal("%s", U_("unable to add event to queue"));
}
}
debug_return;
}
/*
* If std{in,out,err} are not connected to a terminal, interpose
* outselves using a pipe. Fills in io_pipe[][].
*/
static void
interpose_pipes(struct exec_closure *ec, int io_pipe[3][2])
{
bool interpose[3] = { false, false, false };
struct plugin_container *plugin;
bool want_winch = false;
debug_decl(interpose_pipes, SUDO_DEBUG_EXEC);
/*
* Determine whether any of std{in,out,err} or window size changes
* should be logged.
*/
TAILQ_FOREACH(plugin, &io_plugins, entries) {
if (plugin->u.io->log_stdin)
interpose[STDIN_FILENO] = true;
if (plugin->u.io->log_stdout)
interpose[STDOUT_FILENO] = true;
if (plugin->u.io->log_stderr)
interpose[STDERR_FILENO] = true;
if (plugin->u.io->version >= SUDO_API_MKVERSION(1, 12)) {
if (plugin->u.io->change_winsize)
want_winch = true;
}
}
/*
* If stdin, stdout or stderr is not a tty and logging is enabled,
* use a pipe to interpose ourselves.
*/
if (interpose[STDIN_FILENO]) {
if (!isatty(STDIN_FILENO)) {
sudo_debug_printf(SUDO_DEBUG_INFO,
"stdin not a tty, creating a pipe");
if (pipe2(io_pipe[STDIN_FILENO], O_CLOEXEC) != 0)
sudo_fatal("%s", U_("unable to create pipe"));
io_buf_new(STDIN_FILENO, io_pipe[STDIN_FILENO][1],
log_stdin, read_callback, write_callback, ec, &iobufs);
}
}
if (interpose[STDOUT_FILENO]) {
if (!isatty(STDOUT_FILENO)) {
sudo_debug_printf(SUDO_DEBUG_INFO,
"stdout not a tty, creating a pipe");
if (pipe2(io_pipe[STDOUT_FILENO], O_CLOEXEC) != 0)
sudo_fatal("%s", U_("unable to create pipe"));
io_buf_new(io_pipe[STDOUT_FILENO][0], STDOUT_FILENO,
log_stdout, read_callback, write_callback, ec, &iobufs);
}
}
if (interpose[STDERR_FILENO]) {
if (!isatty(STDERR_FILENO)) {
sudo_debug_printf(SUDO_DEBUG_INFO,
"stderr not a tty, creating a pipe");
if (pipe2(io_pipe[STDERR_FILENO], O_CLOEXEC) != 0)
sudo_fatal("%s", U_("unable to create pipe"));
io_buf_new(io_pipe[STDERR_FILENO][0], STDERR_FILENO,
log_stderr, read_callback, write_callback, ec, &iobufs);
}
}
if (want_winch) {
/* Need /dev/tty for SIGWINCH handling. */
io_fds[SFD_USERTTY] = open(_PATH_TTY, O_RDWR);
}
}
/*
* Execute a command and wait for it to finish.
*/
void
exec_nopty(struct command_details *details, struct command_status *cstat)
{
int io_pipe[3][2] = { { -1, -1 }, { -1, -1 }, { -1, -1 } };
int errpipe[2], intercept_sv[2] = { -1, -1 };
struct exec_closure ec = { 0 };
int intercept_sv[2] = { -1, -1 };
sigset_t set, oset;
int errpipe[2];
debug_decl(exec_nopty, SUDO_DEBUG_EXEC);
/*
@@ -329,6 +557,9 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
}
}
/* Interpose std{in,out,err} with pipes if logging I/O. */
interpose_pipes(&ec, io_pipe);
/*
* Block signals until we have our handlers setup in the parent so
* we don't miss SIGCHLD if the command exits immediately.
@@ -363,6 +594,25 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
close(errpipe[0]);
if (intercept_sv[0] != -1)
close(intercept_sv[0]);
/* Replace stdin/stdout/stderr with pipes as needed and exec. */
if (io_pipe[STDIN_FILENO][0] != -1) {
if (dup3(io_pipe[STDIN_FILENO][0], STDIN_FILENO, 0) == -1)
sudo_fatal("dup3");
close(io_pipe[STDIN_FILENO][0]);
close(io_pipe[STDIN_FILENO][1]);
}
if (io_pipe[STDOUT_FILENO][0] != -1) {
if (dup3(io_pipe[STDOUT_FILENO][1], STDOUT_FILENO, 0) == -1)
sudo_fatal("dup3");
close(io_pipe[STDOUT_FILENO][0]);
close(io_pipe[STDOUT_FILENO][1]);
}
if (io_pipe[STDERR_FILENO][0] != -1) {
if (dup3(io_pipe[STDERR_FILENO][1], STDERR_FILENO, 0) == -1)
sudo_fatal("dup3");
close(io_pipe[STDERR_FILENO][0]);
close(io_pipe[STDERR_FILENO][1]);
}
exec_cmnd(details, &oset, intercept_sv[1], errpipe[1]);
while (write(errpipe[1], &errno, sizeof(int)) == -1) {
if (errno != EINTR)
@@ -373,6 +623,13 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
}
sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command,
(int)ec.cmnd_pid);
/* Close the other end of the pipes and socketpairs. */
if (io_pipe[STDIN_FILENO][0] != -1)
close(io_pipe[STDIN_FILENO][0]);
if (io_pipe[STDOUT_FILENO][1] != -1)
close(io_pipe[STDOUT_FILENO][1]);
if (io_pipe[STDERR_FILENO][1] != -1)
close(io_pipe[STDERR_FILENO][1]);
close(errpipe[1]);
if (intercept_sv[1] != -1)
close(intercept_sv[1]);
@@ -412,6 +669,9 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
terminate_command(ec.cmnd_pid, true);
}
/* Enable any I/O log events. */
add_io_events(ec.evbase);
/* Restore signal mask now that signal handlers are setup. */
sigprocmask(SIG_SETMASK, &oset, NULL);
@@ -436,8 +696,13 @@ exec_nopty(struct command_details *details, struct command_status *cstat)
}
#endif
/* Flush any remaining output. */
del_io_events(true);
/* Free things up. */
free_io_bufs();
free_exec_closure(&ec);
debug_return;
}
@@ -488,7 +753,7 @@ handle_sigchld_nopty(struct exec_closure *ec)
/* If the main command is suspended, suspend sudo too. */
if (pid == ec->cmnd_pid)
suspend_sudo_nopty(signo, ec->ppgrp, ec->cmnd_pid);
suspend_sudo_nopty(ec, signo, ec->ppgrp, ec->cmnd_pid);
} else {
if (WIFSIGNALED(status)) {
if (sig2str(WTERMSIG(status), signame) == -1) {

View File

@@ -980,14 +980,10 @@ exec_pty(struct command_details *details, struct command_status *cstat)
debug_decl(exec_pty, SUDO_DEBUG_EXEC);
/*
* Allocate a pty.
* Allocate a pty if sudo is running in a terminal.
*/
if (!pty_setup(details, user_details.tty)) {
if (TAILQ_EMPTY(&io_plugins)) {
/* Not logging I/O and didn't allocate a pty. */
debug_return_bool(false);
}
}
if (!pty_setup(details, user_details.tty))
debug_return_bool(false);
/*
* We communicate with the monitor over a bi-directional pair of sockets.

View File

@@ -219,7 +219,7 @@ main(int argc, char *argv[])
} else if (WIFSTOPPED(status)) {
if (exec_ptrace_stopped(pid, status, &closure)) {
if (pid == child) {
suspend_sudo_nopty(WSTOPSIG(status), ppgrp, child);
suspend_sudo_nopty(NULL, WSTOPSIG(status), ppgrp, child);
if (kill(child, SIGCONT) != 0)
sudo_warn("kill(%d, SIGCONT)", (int)child);
}

View File

@@ -232,6 +232,6 @@ bool set_exec_filter(void);
int exec_ptrace_seize(pid_t child);
/* suspend_nopty.c */
void suspend_sudo_nopty(int signo, pid_t ppgrp, pid_t cmnd_pid);
void suspend_sudo_nopty(struct exec_closure *ec, int signo, pid_t ppgrp, pid_t cmnd_pid);
#endif /* SUDO_EXEC_H */

View File

@@ -34,7 +34,8 @@
#include "sudo_exec.h"
void
suspend_sudo_nopty(int signo, pid_t ppgrp, pid_t cmnd_pid)
suspend_sudo_nopty(struct exec_closure *ec, int signo, pid_t ppgrp,
pid_t cmnd_pid)
{
struct sigaction sa, osa;
pid_t saved_pgrp = -1;
@@ -80,6 +81,9 @@ suspend_sudo_nopty(int signo, pid_t ppgrp, pid_t cmnd_pid)
}
}
/* Log the suspend event. */
log_suspend(ec, signo);
if (signo == SIGTSTP) {
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
@@ -94,6 +98,10 @@ suspend_sudo_nopty(int signo, pid_t ppgrp, pid_t cmnd_pid)
if (sudo_sigaction(SIGTSTP, &osa, NULL) != 0)
sudo_warn(U_("unable to restore handler for signal %d"), SIGTSTP);
}
/* Log the resume event. */
log_suspend(ec, SIGCONT);
if (saved_pgrp != -1) {
/*
* On resume, restore foreground process group, if different.