diff options
Diffstat (limited to 'src/exec_iolog.c')
-rw-r--r-- | src/exec_iolog.c | 613 |
1 files changed, 613 insertions, 0 deletions
diff --git a/src/exec_iolog.c b/src/exec_iolog.c new file mode 100644 index 0000000..8575ee4 --- /dev/null +++ b/src/exec_iolog.c @@ -0,0 +1,613 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2009-2022 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 <stdlib.h> +#include <string.h> +#include <unistd.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" + +sigset_t ttyblock; +int ttymode = TERM_COOKED; + +struct io_buffer_list iobufs = SLIST_HEAD_INITIALIZER(&iobufs); + +int io_fds[6] = { -1, -1, -1, -1, -1, -1 }; + +/* + * Remove and free any events associated with the specified + * file descriptor present in the I/O buffers list. + */ +void +ev_free_by_fd(struct sudo_event_base *evbase, int fd) +{ + struct io_buffer *iob; + debug_decl(ev_free_by_fd, SUDO_DEBUG_EXEC); + + /* Deschedule any users of the fd and free up the events. */ + SLIST_FOREACH(iob, &iobufs, entries) { + if (iob->revent != NULL) { + if (sudo_ev_get_fd(iob->revent) == fd) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing revent %p with fd %d", + __func__, iob->revent, fd); + sudo_ev_free(iob->revent); + iob->revent = NULL; + } + } + if (iob->wevent != NULL) { + if (sudo_ev_get_fd(iob->wevent) == fd) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing wevent %p with fd %d", + __func__, iob->wevent, fd); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + } + } + } + debug_return; +} + +/* + * Only close the fd if it is not /dev/tty or std{in,out,err}. + * Return value is the same as close(2). + */ +int +safe_close(int fd) +{ + debug_decl(safe_close, SUDO_DEBUG_EXEC); + + /* Avoid closing /dev/tty or std{in,out,err}. */ + if (fd < 3 || fd == io_fds[SFD_USERTTY]) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: not closing fd %d (%s)", __func__, fd, _PATH_TTY); + errno = EINVAL; + debug_return_int(-1); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: closing fd %d", __func__, fd); + debug_return_int(close(fd)); +} + +/* + * Allocate a new I/O buffer and associated read/write events. + */ +void +io_buf_new(int rfd, int wfd, + bool (*action)(const char *, unsigned int, struct io_buffer *), + void (*read_cb)(int fd, int what, void *v), + void (*write_cb)(int fd, int what, void *v), + struct exec_closure *ec, struct io_buffer_list *head) +{ + int n; + struct io_buffer *iob; + debug_decl(io_buf_new, SUDO_DEBUG_EXEC); + + /* Set non-blocking mode. */ + n = fcntl(rfd, F_GETFL, 0); + if (n != -1 && !ISSET(n, O_NONBLOCK)) + (void) fcntl(rfd, F_SETFL, n | O_NONBLOCK); + n = fcntl(wfd, F_GETFL, 0); + if (n != -1 && !ISSET(n, O_NONBLOCK)) + (void) fcntl(wfd, F_SETFL, n | O_NONBLOCK); + + /* Allocate and add to head of list. */ + if ((iob = malloc(sizeof(*iob))) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + iob->ec = ec; + iob->revent = sudo_ev_alloc(rfd, SUDO_EV_READ|SUDO_EV_PERSIST, + read_cb, iob); + iob->wevent = sudo_ev_alloc(wfd, SUDO_EV_WRITE|SUDO_EV_PERSIST, + write_cb, iob); + iob->len = 0; + iob->off = 0; + iob->action = action; + iob->buf[0] = '\0'; + if (iob->revent == NULL || iob->wevent == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + SLIST_INSERT_HEAD(head, iob, entries); + + debug_return; +} + +/* + * Schedule I/O events before starting the main event loop or + * resuming from suspend. + */ +void +add_io_events(struct sudo_event_base *evbase) +{ + struct io_buffer *iob; + debug_decl(add_io_events, SUDO_DEBUG_EXEC); + + /* + * 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 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("%s", U_("unable to add event to queue")); + } + } + if (iob->wevent != NULL) { + /* Enable writer if buffer is not empty. */ + 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("%s", 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. + */ +void +del_io_events(bool nonblocking) +{ + 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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* 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("%s", 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("%s", U_("unable to add event to queue")); + } + } + } + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: flushing remaining I/O buffers (nonblocking)", __func__); + (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("%s", U_("unable to add event to queue")); + } + } + } + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: flushing remaining write buffers (blocking)", __func__); + (void) sudo_ev_dispatch(evbase); + + /* 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); + } + } + } + } + + /* Free temporary event base, removing its events. */ + sudo_ev_base_free(evbase); + + debug_return; +} + +/* + * Free the contents of the I/O buffers queue. + */ +void +free_io_bufs(void) +{ + struct io_buffer *iob; + debug_decl(free_io_bufs, SUDO_DEBUG_EXEC); + + while ((iob = SLIST_FIRST(&iobufs)) != NULL) { + SLIST_REMOVE_HEAD(&iobufs, entries); + if (iob->revent != NULL) + sudo_ev_free(iob->revent); + if (iob->wevent != NULL) + sudo_ev_free(iob->wevent); + free(iob); + } + + debug_return; +} + +/* Call I/O plugin tty input log method. */ +bool +log_ttyin(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_ttyin, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_ttyin) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_ttyin(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_ttyin = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stdin log method. */ +bool +log_stdin(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stdin, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stdin) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stdin(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stdin = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin tty output log method. */ +bool +log_ttyout(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_ttyout, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_ttyout) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_ttyout(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_ttyout = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's tty) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing devtty wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stdout log method. */ +bool +log_stdout(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stdout, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stdout) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stdout(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stdout = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's stdout) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing stdout wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin stderr log method. */ +bool +log_stderr(const char *buf, unsigned int n, struct io_buffer *iob) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + bool ret = true; + debug_decl(log_stderr, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->log_stderr) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_stderr(buf, n, &errstr); + if (rc <= 0) { + if (rc < 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_stderr = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("I/O plugin error"), + iob->ec->details->info); + } else { + audit_reject(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("command rejected by I/O plugin"), + iob->ec->details->info); + } + ret = false; + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + if (!ret) { + /* + * I/O plugin rejected the output, delete the write event + * (user's stderr) so we do not display the rejected output. + */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: deleting and freeing stderr wevent %p", __func__, iob->wevent); + sudo_ev_free(iob->wevent); + iob->wevent = NULL; + iob->off = iob->len = 0; + } + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return_bool(ret); +} + +/* Call I/O plugin suspend log method. */ +void +log_suspend(struct exec_closure *ec, int signo) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + debug_decl(log_suspend, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->version < SUDO_API_MKVERSION(1, 13)) + continue; + if (plugin->u.io->log_suspend) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->log_suspend(signo, &errstr); + if (rc <= 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->log_suspend = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("error logging suspend"), + ec->details->info); + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return; +} + +/* Call I/O plugin window change log method. */ +void +log_winchange(struct exec_closure *ec, unsigned int rows, + unsigned int cols) +{ + struct plugin_container *plugin; + const char *errstr = NULL; + sigset_t omask; + debug_decl(log_winchange, SUDO_DEBUG_EXEC); + + sigprocmask(SIG_BLOCK, &ttyblock, &omask); + TAILQ_FOREACH(plugin, &io_plugins, entries) { + if (plugin->u.io->version < SUDO_API_MKVERSION(1, 12)) + continue; + if (plugin->u.io->change_winsize) { + int rc; + + sudo_debug_set_active_instance(plugin->debug_instance); + rc = plugin->u.io->change_winsize(rows, cols, &errstr); + if (rc <= 0) { + /* Error: disable plugin's I/O function. */ + plugin->u.io->change_winsize = NULL; + audit_error(plugin->name, SUDO_IO_PLUGIN, + errstr ? errstr : _("error changing window size"), + ec->details->info); + break; + } + } + } + sudo_debug_set_active_instance(sudo_debug_instance); + sigprocmask(SIG_SETMASK, &omask, NULL); + + debug_return; +} + +void +init_ttyblock(void) +{ + /* So we can block tty-generated signals */ + sigemptyset(&ttyblock); + sigaddset(&ttyblock, SIGINT); + sigaddset(&ttyblock, SIGQUIT); + sigaddset(&ttyblock, SIGTSTP); + sigaddset(&ttyblock, SIGTTIN); + sigaddset(&ttyblock, SIGTTOU); +} |