diff --git a/MANIFEST b/MANIFEST index db5c821a1..747c4afeb 100644 --- a/MANIFEST +++ b/MANIFEST @@ -359,7 +359,9 @@ plugins/sudoers/insults.h plugins/sudoers/interfaces.c plugins/sudoers/interfaces.h plugins/sudoers/iolog.c +plugins/sudoers/iolog_client.c plugins/sudoers/iolog_path_escapes.c +plugins/sudoers/iolog_plugin.h plugins/sudoers/ldap.c plugins/sudoers/ldap_conf.c plugins/sudoers/ldap_util.c diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in index 14552ce65..c85e348bc 100644 --- a/plugins/sudoers/Makefile.in +++ b/plugins/sudoers/Makefile.in @@ -54,9 +54,10 @@ INSTALL_BACKUP = @INSTALL_BACKUP@ # Libraries LIBUTIL = $(top_builddir)/lib/util/libsudo_util.la LIBIOLOG = $(top_builddir)/lib/iolog/libsudo_iolog.la +LIBLOGSRV = $(top_builddir)/lib/logsrv/liblogsrv.la LIBS = $(LIBUTIL) NET_LIBS = @NET_LIBS@ -SUDOERS_LIBS = @SUDOERS_LIBS@ @AFS_LIBS@ @GETGROUPS_LIB@ $(NET_LIBS) $(LIBIOLOG) +SUDOERS_LIBS = @SUDOERS_LIBS@ @AFS_LIBS@ @GETGROUPS_LIB@ $(NET_LIBS) $(LIBIOLOG) $(LIBLOGSRV) REPLAY_LIBS = @REPLAY_LIBS@ $(LIBIOLOG) VISUDO_LIBS = $(NET_LIBS) CVTSUDOERS_LIBS = $(NET_LIBS) @@ -158,17 +159,17 @@ LIBPARSESUDOERS_OBJS = alias.lo audit.lo base64.lo defaults.lo digestname.lo \ filedigest.lo gentime.lo gmtoff.lo gram.lo hexchar.lo \ match.lo match_addr.lo match_command.lo match_digest.lo \ pwutil.lo pwutil_impl.lo rcstr.lo redblack.lo \ - sudoers_debug.lo timeout.lo timestr.lo toke.lo \ - toke_util.lo + strlist.lo sudoers_debug.lo timeout.lo timestr.lo \ + toke.lo toke_util.lo LIBPARSESUDOERS_IOBJS = $(LIBPARSESUDOERS_OBJS:.lo=.i) passwd.i SUDOERS_OBJS = $(AUTH_OBJS) boottime.lo check.lo editor.lo env.lo \ env_pattern.lo file.lo find_path.lo fmtsudoers.lo gc.lo \ goodpath.lo group_plugin.lo interfaces.lo iolog.lo \ - iolog_path_escapes.lo locale.lo logging.lo logwrap.lo \ - parse.lo policy.lo prompt.lo set_perms.lo starttime.lo \ - sudo_nss.lo sudoers.lo timestamp.lo @SUDOERS_OBJS@ + iolog_path_escapes.lo locale.lo iolog_client.lo logging.lo \ + logwrap.lo parse.lo policy.lo prompt.lo set_perms.lo \ + starttime.lo sudo_nss.lo sudoers.lo timestamp.lo @SUDOERS_OBJS@ SUDOERS_IOBJS = $(SUDOERS_OBJS:.lo=.i) @@ -179,17 +180,17 @@ VISUDO_IOBJS = sudo_printf.i visudo.i CVTSUDOERS_OBJS = cvtsudoers.o cvtsudoers_json.o cvtsudoers_ldif.o \ cvtsudoers_pwutil.o fmtsudoers.lo locale.lo parse_ldif.o \ - strlist.o stubs.o sudo_printf.o ldap_util.lo + stubs.o sudo_printf.o ldap_util.lo CVTSUDOERS_IOBJS = cvtsudoers.i cvtsudoers_json.i cvtsudoers_ldif.i \ - cvtsudoers_pwutil.i strlist.i + cvtsudoers_pwutil.i REPLAY_OBJS = getdate.o sudoreplay.o REPLAY_IOBJS = $(REPLAY_OBJS:.o=.i) TEST_OBJS = fmtsudoers.lo group_plugin.lo interfaces.lo ldap_util.lo \ - locale.lo net_ifs.o parse_ldif.o strlist.o sudo_printf.o \ + locale.lo net_ifs.o parse_ldif.o sudo_printf.o \ testsudoers.o tsgetgrpw.o IOBJS = $(LIBPARSESUDOERS_IOBJS) $(SUDOERS_IOBJS) $(VISUDO_IOBJS) \ @@ -214,8 +215,9 @@ CHECK_GENTIME_OBJS = check_gentime.o gentime.lo gmtoff.lo sudoers_debug.lo CHECK_HEXCHAR_OBJS = check_hexchar.o hexchar.lo sudoers_debug.lo -CHECK_IOLOG_PLUGIN_OBJS = check_iolog_plugin.o iolog.lo locale.lo pwutil.lo \ - pwutil_impl.lo redblack.lo sudoers_debug.lo +CHECK_IOLOG_PLUGIN_OBJS = check_iolog_plugin.o iolog.lo iolog_client.lo \ + locale.lo pwutil.lo pwutil_impl.lo redblack.lo \ + strlist.lo sudoers_debug.lo CHECK_SYMBOLS_OBJS = check_symbols.o @@ -266,7 +268,7 @@ Makefile: $(srcdir)/Makefile.in libparsesudoers.la: $(LIBPARSESUDOERS_OBJS) $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(LIBPARSESUDOERS_OBJS) -no-install -sudoers.la: $(SUDOERS_OBJS) $(LIBUTIL) $(LIBIOLOG) libparsesudoers.la @LT_LDDEP@ +sudoers.la: $(SUDOERS_OBJS) $(LIBUTIL) $(LIBIOLOG) $(LIBLOGSRV) libparsesudoers.la @LT_LDDEP@ case "$(LT_LDFLAGS)" in \ *-no-install*) \ $(LIBTOOL) $(LTFLAGS) @LT_STATIC@ --mode=link $(CC) $(LDFLAGS) $(LT_LDFLAGS) -o $@ $(SUDOERS_OBJS) libparsesudoers.la $(SUDOERS_LIBS) -module;; \ @@ -310,8 +312,8 @@ check_gentime: $(CHECK_GENTIME_OBJS) $(LIBUTIL) check_hexchar: $(CHECK_HEXCHAR_OBJS) $(LIBUTIL) $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_HEXCHAR_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) -check_iolog_plugin: $(CHECK_IOLOG_PLUGIN_OBJS) $(LIBUTIL) $(LIBIOLOG) - $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_PLUGIN_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBIOLOG) +check_iolog_plugin: $(CHECK_IOLOG_PLUGIN_OBJS) $(LIBUTIL) $(LIBIOLOG) $(LIBLOGSRV) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_IOLOG_PLUGIN_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBIOLOG) $(LIBLOGSRV) check_starttime: $(CHECK_STARTTIME_OBJS) $(LIBUTIL) $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_STARTTIME_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS) @@ -1494,25 +1496,57 @@ interfaces.i: $(srcdir)/interfaces.c $(devdir)/def_data.h \ interfaces.plog: interfaces.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/interfaces.c --i-file $< --output-file $@ iolog.lo: $(srcdir)/iolog.c $(devdir)/def_data.h $(incdir)/compat/stdbool.h \ + $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/defaults.h \ - $(srcdir)/logging.h $(srcdir)/parse.h $(srcdir)/sudo_nss.h \ - $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ - $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(srcdir)/iolog_plugin.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/strlist.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog.c iolog.i: $(srcdir)/iolog.c $(devdir)/def_data.h $(incdir)/compat/stdbool.h \ + $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/defaults.h \ - $(srcdir)/logging.h $(srcdir)/parse.h $(srcdir)/sudo_nss.h \ - $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ - $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(srcdir)/iolog_plugin.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/strlist.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h $(CC) -E -o $@ $(CPPFLAGS) $< iolog.plog: iolog.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog.c --i-file $< --output-file $@ +iolog_client.lo: $(srcdir)/iolog_client.c $(devdir)/def_data.h \ + $(incdir)/compat/getaddrinfo.h $(incdir)/compat/stdbool.h \ + $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.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)/defaults.h \ + $(srcdir)/iolog_plugin.h $(srcdir)/logging.h \ + $(srcdir)/parse.h $(srcdir)/strlist.h $(srcdir)/sudo_nss.h \ + $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_client.c +iolog_client.i: $(srcdir)/iolog_client.c $(devdir)/def_data.h \ + $(incdir)/compat/getaddrinfo.h $(incdir)/compat/stdbool.h \ + $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.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)/defaults.h \ + $(srcdir)/iolog_plugin.h $(srcdir)/logging.h \ + $(srcdir)/parse.h $(srcdir)/strlist.h $(srcdir)/sudo_nss.h \ + $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +iolog_client.plog: iolog_client.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_client.c --i-file $< --output-file $@ iolog_path_escapes.lo: $(srcdir)/iolog_path_escapes.c $(devdir)/def_data.h \ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ @@ -2201,15 +2235,15 @@ starttime.i: $(srcdir)/starttime.c $(devdir)/def_data.h \ $(CC) -E -o $@ $(CPPFLAGS) $< starttime.plog: starttime.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/starttime.c --i-file $< --output-file $@ -strlist.o: $(srcdir)/strlist.c $(incdir)/compat/stdbool.h \ - $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/strlist.h \ - $(srcdir)/sudoers_debug.h $(top_builddir)/config.h - $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/strlist.c +strlist.lo: $(srcdir)/strlist.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/strlist.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/strlist.c strlist.i: $(srcdir)/strlist.c $(incdir)/compat/stdbool.h \ - $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/strlist.h \ - $(srcdir)/sudoers_debug.h $(top_builddir)/config.h + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/strlist.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h $(CC) -E -o $@ $(CPPFLAGS) $< strlist.plog: strlist.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/strlist.c --i-file $< --output-file $@ diff --git a/plugins/sudoers/def_data.c b/plugins/sudoers/def_data.c index 9d1bb86f5..00ce8ce5e 100644 --- a/plugins/sudoers/def_data.c +++ b/plugins/sudoers/def_data.c @@ -505,6 +505,14 @@ struct sudo_defs_types sudo_defs_table[] = { "log_denied", T_FLAG, N_("Log when a command is denied by sudoers"), NULL, + }, { + "log_server", T_LIST|T_BOOL, + N_("Sudo log server(s) to connect to with optional port"), + NULL, + }, { + "log_server_timeout", T_TIMEOUT|T_BOOL, + N_("Sudo log server timeout in seconds: %u"), + NULL, }, { NULL, 0, NULL } diff --git a/plugins/sudoers/def_data.h b/plugins/sudoers/def_data.h index d8393bbbd..bc41e7fc5 100644 --- a/plugins/sudoers/def_data.h +++ b/plugins/sudoers/def_data.h @@ -232,6 +232,10 @@ #define def_log_allowed (sudo_defs_table[I_LOG_ALLOWED].sd_un.flag) #define I_LOG_DENIED 116 #define def_log_denied (sudo_defs_table[I_LOG_DENIED].sd_un.flag) +#define I_LOG_SERVER 117 +#define def_log_server (sudo_defs_table[I_LOG_SERVER].sd_un.list) +#define I_LOG_SERVER_TIMEOUT 118 +#define def_log_server_timeout (sudo_defs_table[I_LOG_SERVER_TIMEOUT].sd_un.ival) enum def_tuple { never, diff --git a/plugins/sudoers/def_data.in b/plugins/sudoers/def_data.in index cfd5ebb7c..2004b0d7d 100644 --- a/plugins/sudoers/def_data.in +++ b/plugins/sudoers/def_data.in @@ -366,3 +366,9 @@ log_allowed log_denied T_FLAG "Log when a command is denied by sudoers" +log_server + T_LIST|T_BOOL + "Sudo log server(s) to connect to with optional port" +log_server_timeout + T_TIMEOUT|T_BOOL + "Sudo log server timeout in seconds: %u" diff --git a/plugins/sudoers/defaults.c b/plugins/sudoers/defaults.c index 6fd3899b5..1d6f22ed2 100644 --- a/plugins/sudoers/defaults.c +++ b/plugins/sudoers/defaults.c @@ -562,6 +562,7 @@ init_defaults(void) #ifdef HAVE_ZLIB_H def_compress_io = true; #endif + def_log_server_timeout = 30; def_ignore_audit_errors = true; def_ignore_iolog_errors = false; def_ignore_logfile_errors = true; diff --git a/plugins/sudoers/iolog.c b/plugins/sudoers/iolog.c index ad2fbf3a4..0e7bb3efc 100644 --- a/plugins/sudoers/iolog.c +++ b/plugins/sudoers/iolog.c @@ -43,23 +43,7 @@ #include "sudoers.h" #include "sudo_iolog.h" - -/* XXX - separate sudoers.h and iolog.h? */ -#undef runas_pw -#undef runas_gr - -struct iolog_details { - const char *cwd; - const char *tty; - const char *user; - const char *command; - const char *iolog_path; - struct passwd *runas_pw; - struct group *runas_gr; - int lines; - int cols; - bool ignore_iolog_errors; -}; +#include "iolog_plugin.h" static struct iolog_file iolog_files[] = { { false }, /* IOFD_STDIN */ @@ -70,6 +54,7 @@ static struct iolog_file iolog_files[] = { { true, }, /* IOFD_TIMING */ }; +static struct client_closure client_closure = CLIENT_CLOSURE_INITIALIZER(client_closure); static struct iolog_details iolog_details; static bool warned = false; static struct timespec last_time; @@ -77,6 +62,8 @@ static struct timespec last_time; /* sudoers_io is declared at the end of this file. */ extern __dso_public struct io_plugin sudoers_io; +#define iolog_remote (client_closure.sock != -1) + /* * Sudoers callback for maxseq Defaults setting. */ @@ -162,10 +149,47 @@ cb_iolog_mode(const union sudo_defs_val *sd_un) } /* - * Pull out I/O log related data from user_info and command_info arrays. - * Returns true if I/O logging is enabled, else false. + * Convert a comma-separated list to a string list. */ -static bool +static struct sudoers_str_list * +deserialize_stringlist(const char *s) +{ + struct sudoers_str_list *strlist; + struct sudoers_string *str; + const char *s_end = s + strlen(s); + const char *cp, *ep; + debug_decl(deserialize_stringlist, SUDOERS_DEBUG_UTIL) + + if ((strlist = str_list_alloc()) == NULL) + debug_return_ptr(NULL); + + for (cp = sudo_strsplit(s, s_end, ",", &ep); cp != NULL; + cp = sudo_strsplit(NULL, s_end, ",", &ep)) { + if (cp == ep) + continue; + if ((str = malloc(sizeof(*str))) == NULL) + goto bad; + if ((str->str = strndup(cp, (ep - cp))) == NULL) { + free(str); + goto bad; + } + STAILQ_INSERT_TAIL(strlist, str, entries); + } + if (STAILQ_EMPTY(strlist)) + goto bad; + + debug_return_ptr(strlist); + +bad: + str_list_free(strlist); + debug_return_ptr(NULL); +} + +/* + * Pull out I/O log related data from user_info and command_info arrays. + * Returns true if I/O logging is enabled, false if not and -1 on error. + */ +static int iolog_deserialize_info(struct iolog_details *details, char * const user_info[], char * const command_info[]) { @@ -197,6 +221,12 @@ iolog_deserialize_info(struct iolog_details *details, char * const user_info[], continue; } break; + case 'h': + if (strncmp(*cur, "host=", sizeof("host=") - 1) == 0) { + details->host = *cur + sizeof("host=") - 1; + continue; + } + break; case 'l': if (strncmp(*cur, "lines=", sizeof("lines=") - 1) == 0) { int n = sudo_strtonum(*cur + sizeof("lines=") - 1, 1, INT_MAX, @@ -319,6 +349,21 @@ iolog_deserialize_info(struct iolog_details *details, char * const user_info[], continue; } break; + case 'l': + if (strncmp(*cur, "log_servers=", sizeof("log_servers=") - 1) == 0) { + details->log_servers = + deserialize_stringlist(*cur + sizeof("log_servers=") - 1); + if (!details->log_servers) + goto oom; + continue; + } + if (strncmp(*cur, "log_server_timeout=", sizeof("log_server_timeout=") - 1) == 0) { + details->server_timeout.tv_sec = + sudo_strtonum(*cur + sizeof("log_server_timeout=") - 1, 1, + TIME_T_MAX, NULL); + continue; + } + break; case 'm': if (strncmp(*cur, "maxseq=", sizeof("maxseq=") - 1) == 0) { union sudo_defs_val sd_un; @@ -385,10 +430,14 @@ iolog_deserialize_info(struct iolog_details *details, char * const user_info[], details->runas_gr = sudo_fakegrnam(idbuf); } } - debug_return_bool( + debug_return_int( iolog_files[IOFD_STDIN].enabled || iolog_files[IOFD_STDOUT].enabled || iolog_files[IOFD_STDERR].enabled || iolog_files[IOFD_TTYIN].enabled || iolog_files[IOFD_TTYOUT].enabled); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + str_list_free(details->log_servers); + debug_return_int(-1); } /* @@ -396,8 +445,7 @@ iolog_deserialize_info(struct iolog_details *details, char * const user_info[], * This file is not compressed. */ static bool -write_info_log(int dfd, char *iolog_dir, struct iolog_details *details, - char * const argv[]) +write_info_log(int dfd, char *iolog_dir, struct iolog_details *details) { struct iolog_info iolog_info; debug_decl(write_info_log, SUDOERS_DEBUG_UTIL) @@ -414,7 +462,7 @@ write_info_log(int dfd, char *iolog_dir, struct iolog_details *details, iolog_info.lines = details->lines; iolog_info.cols = details->cols; - if (!iolog_write_info_file(dfd, iolog_dir, &iolog_info, argv)) { + if (!iolog_write_info_file(dfd, iolog_dir, &iolog_info, details->argv)) { log_warning(SLOG_SEND_MAIL, N_("unable to write to I/O log file: %s"), strerror(errno)); warned = true; @@ -424,55 +472,13 @@ write_info_log(int dfd, char *iolog_dir, struct iolog_details *details, } static int -sudoers_io_open(unsigned int version, sudo_conv_t conversation, - sudo_printf_t plugin_printf, char * const settings[], - char * const user_info[], char * const command_info[], - int argc, char * const argv[], char * const user_env[], char * const args[]) +sudoers_io_open_local(void) { - struct sudo_conf_debug_file_list debug_files = TAILQ_HEAD_INITIALIZER(debug_files); char iolog_path[PATH_MAX], sessid[7]; - char * const *cur; - const char *cp, *plugin_path = NULL; size_t len; - int i, ret = -1; int iolog_dir_fd = -1; - debug_decl(sudoers_io_open, SUDOERS_DEBUG_PLUGIN) - - sudo_conv = conversation; - sudo_printf = plugin_printf; - - /* If we have no command (because -V was specified) just return. */ - if (argc == 0) - debug_return_int(true); - - bindtextdomain("sudoers", LOCALEDIR); - - /* Initialize the debug subsystem. */ - for (cur = settings; (cp = *cur) != NULL; cur++) { - if (strncmp(cp, "debug_flags=", sizeof("debug_flags=") - 1) == 0) { - cp += sizeof("debug_flags=") - 1; - if (!sudoers_debug_parse_flags(&debug_files, cp)) - debug_return_int(-1); - continue; - } - if (strncmp(cp, "plugin_path=", sizeof("plugin_path=") - 1) == 0) { - plugin_path = cp + sizeof("plugin_path=") - 1; - continue; - } - } - - if (!sudoers_debug_register(plugin_path, &debug_files)) { - ret = -1; - goto done; - } - - /* - * Pull iolog settings out of command_info. - */ - if (!iolog_deserialize_info(&iolog_details, user_info, command_info)) { - ret = false; - goto done; - } + int i, ret = -1; + debug_decl(sudoers_io_open_local, SUDOERS_DEBUG_PLUGIN) /* If no I/O log path defined we need to figure it out ourselves. */ if (iolog_details.iolog_path == NULL) { @@ -517,7 +523,7 @@ sudoers_io_open(unsigned int version, sudo_conv_t conversation, } /* Write log file with user and command details. */ - if (!write_info_log(iolog_dir_fd, iolog_path, &iolog_details, argv)) + if (!write_info_log(iolog_dir_fd, iolog_path, &iolog_details)) goto done; /* Create the timing and I/O log files. */ @@ -529,6 +535,107 @@ sudoers_io_open(unsigned int version, sudo_conv_t conversation, } } + ret = true; + +done: + if (iolog_dir_fd != -1) + close(iolog_dir_fd); + + debug_return_int(ret); +} + +static int +sudoers_io_open_remote(void) +{ + int sock, ret = -1; + debug_decl(sudoers_io_open_remote, SUDOERS_DEBUG_PLUGIN) + + /* Connect to log server. */ + sock = log_server_connect(iolog_details.log_servers, + &iolog_details.server_timeout); + if (sock == -1) { + /* TODO: support offline logs if server unreachable */ + sudo_warnx(U_("unable to connect to log server")); + ret = -1; + goto done; + } + + if (!client_closure_fill(&client_closure, sock, &iolog_details, &sudoers_io)) { + close(sock); + ret = -1; + goto done; + } + + /* Enable reader for server hello */ + ret = client_closure.read_ev->add(client_closure.read_ev, + &iolog_details.server_timeout); + if (ret == -1) + sudo_warn(U_("unable to add event to queue")); + +done: + debug_return_int(ret); +} + +static int +sudoers_io_open(unsigned int version, sudo_conv_t conversation, + sudo_printf_t plugin_printf, char * const settings[], + char * const user_info[], char * const command_info[], + int argc, char * const argv[], char * const user_env[], char * const args[]) +{ + struct sudo_conf_debug_file_list debug_files = TAILQ_HEAD_INITIALIZER(debug_files); + char * const *cur; + const char *cp, *plugin_path = NULL; + int ret = -1; + debug_decl(sudoers_io_open, SUDOERS_DEBUG_PLUGIN) + + sudo_conv = conversation; + sudo_printf = plugin_printf; + + /* If we have no command (because -V was specified) just return. */ + if (argc == 0) + debug_return_int(true); + + bindtextdomain("sudoers", LOCALEDIR); + + /* Initialize the debug subsystem. */ + for (cur = settings; (cp = *cur) != NULL; cur++) { + if (strncmp(cp, "debug_flags=", sizeof("debug_flags=") - 1) == 0) { + cp += sizeof("debug_flags=") - 1; + if (!sudoers_debug_parse_flags(&debug_files, cp)) + debug_return_int(-1); + continue; + } + if (strncmp(cp, "plugin_path=", sizeof("plugin_path=") - 1) == 0) { + plugin_path = cp + sizeof("plugin_path=") - 1; + continue; + } + } + + if (!sudoers_debug_register(plugin_path, &debug_files)) { + ret = -1; + goto done; + } + + /* + * Pull iolog settings out of command_info. + */ + ret = iolog_deserialize_info(&iolog_details, user_info, command_info); + if (ret != true) + goto done; + iolog_details.user_env = user_env; + iolog_details.argv = argv; + iolog_details.argc = argc; + + /* + * Create local I/O log file or connect to remote log server. + */ + if (sudoers_io.event_alloc != NULL && iolog_details.log_servers != NULL) + ret = sudoers_io_open_remote(); + else + ret = sudoers_io_open_local(); + if (ret != true) + goto done; + /* * Clear I/O log function pointers for disabled log functions. */ @@ -548,18 +655,19 @@ sudoers_io_open(unsigned int version, sudo_conv_t conversation, "%s: unable to get time of day", __func__); goto done; } - - ret = true; + if (iolog_remote) + client_closure.start_time = last_time; done: - if (iolog_dir_fd != -1) - close(iolog_dir_fd); - if (iolog_details.runas_pw) - sudo_pw_delref(iolog_details.runas_pw); - if (iolog_details.runas_gr) - sudo_gr_delref(iolog_details.runas_gr); - sudo_freepwcache(); - sudo_freegrcache(); + if (ret != true) { + client_closure_free(&client_closure); + if (iolog_details.runas_pw) + sudo_pw_delref(iolog_details.runas_pw); + if (iolog_details.runas_gr) + sudo_gr_delref(iolog_details.runas_gr); + sudo_freepwcache(); + sudo_freegrcache(); + } /* Ignore errors if they occur if the policy says so. */ if (ret == -1 && iolog_details.ignore_iolog_errors) @@ -575,12 +683,36 @@ sudoers_io_close(int exit_status, int error) int i; debug_decl(sudoers_io_close, SUDOERS_DEBUG_PLUGIN) - for (i = 0; i < IOFD_MAX; i++) { - if (iolog_files[i].fd.v == NULL) - continue; - iolog_close(&iolog_files[i], &errstr); + if (iolog_remote) { + if (client_closure.disabled) + goto done; + + if (!fmt_exit_message(&client_closure, exit_status, error)) + goto done; + + /* + * Main sudo event loop exited, use our own mini event loop + * to flush the write queue and read the final commit messages. + */ + if (!client_loop(&client_closure)) + goto done; + } else { + for (i = 0; i < IOFD_MAX; i++) { + if (iolog_files[i].fd.v == NULL) + continue; + iolog_close(&iolog_files[i], &errstr); + } } +done: + client_closure_free(&client_closure); + if (iolog_details.runas_pw) + sudo_pw_delref(iolog_details.runas_pw); + if (iolog_details.runas_gr) + sudo_gr_delref(iolog_details.runas_gr); + sudo_freepwcache(); + sudo_freegrcache(); + if (errstr != NULL && !warned) { /* Only warn about I/O log file errors once. */ log_warning(SLOG_SEND_MAIL, @@ -605,52 +737,128 @@ sudoers_io_version(int verbose) } /* - * Generic I/O logging function. Called by the I/O logging entry points. + * Write an I/O log entry to the local file system. * Returns 1 on success and -1 on error. + * Fills in errstr on error. */ static int -sudoers_io_log(struct iolog_file *iol, const char *buf, unsigned int len, - int event) +sudoers_io_log_local(int event, const char *buf, unsigned int len, + struct timespec *delay, const char **errstr) { - struct timespec now, delay; + struct iolog_file *iol; char tbuf[1024]; - const char *errstr = NULL; int ret = -1; - debug_decl(sudoers_io_log, SUDOERS_DEBUG_PLUGIN) + debug_decl(sudoers_io_log_local, SUDOERS_DEBUG_PLUGIN) + if (event < 0 || event >= IOFD_MAX) { + *errstr = NULL; + sudo_warnx(U_("unexpected I/O event %d"), event); + debug_return_int(-1); + } + iol = &iolog_files[event]; if (!iol->enabled) { + *errstr = NULL; sudo_warnx(U_("%s: internal error, I/O log file for event %d not open"), __func__, event); debug_return_int(-1); } + /* Write I/O log file entry. */ + if (iolog_write(iol, buf, len, errstr) == -1) + goto done; + + /* Write timing file entry. */ + len = (unsigned int)snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %u\n", + event, (long long)delay->tv_sec, delay->tv_nsec, len); + if (len >= sizeof(tbuf)) { + /* Not actually possible due to the size of tbuf[]. */ + *errstr = strerror(EOVERFLOW); + goto done; + } + if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, errstr) == -1) + goto done; + + /* Success. */ + ret = 1; + +done: + debug_return_int(ret); +} + +/* + * Schedule an I/O log entry to be written to the log server. + * Returns 1 on success and -1 on error. + * Fills in errstr on error. + */ +static int +sudoers_io_log_remote(int event, const char *buf, unsigned int len, + struct timespec *delay, const char **errstr) +{ + int type, ret = -1; + debug_decl(sudoers_io_log_remote, SUDOERS_DEBUG_PLUGIN) + + if (client_closure.disabled) + debug_return_int(1); + + /* Track elapsed time for comparison with commit points. */ + sudo_timespecadd(delay, &client_closure.elapsed, &client_closure.elapsed); + + switch (event) { + case IO_EVENT_STDIN: + type = CLIENT_MESSAGE__TYPE_STDIN_BUF; + break; + case IO_EVENT_STDOUT: + type = CLIENT_MESSAGE__TYPE_STDOUT_BUF; + break; + case IO_EVENT_STDERR: + type = CLIENT_MESSAGE__TYPE_STDERR_BUF; + break; + case IO_EVENT_TTYIN: + type = CLIENT_MESSAGE__TYPE_TTYIN_BUF; + break; + case IO_EVENT_TTYOUT: + type = CLIENT_MESSAGE__TYPE_TTYOUT_BUF; + break; + default: + sudo_warnx(U_("unexpected I/O event %d"), event); + goto done; + } + if (fmt_io_buf(&client_closure, type, buf, len, delay)) { + ret = client_closure.write_ev->add(client_closure.write_ev, + &iolog_details.server_timeout); + if (ret == -1) + sudo_warn(U_("unable to add event to queue")); + } + +done: + debug_return_int(ret); +} + +/* + * Generic I/O logging function. Called by the I/O logging entry points. + * Returns 1 on success and -1 on error. + */ +static int +sudoers_io_log(const char *buf, unsigned int len, int event) +{ + struct timespec now, delay; + const char *errstr = NULL; + int ret = -1; + debug_decl(sudoers_io_log, SUDOERS_DEBUG_PLUGIN) + if (sudo_gettime_awake(&now) == -1) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO, "%s: unable to get time of day", __func__); errstr = strerror(errno); goto bad; } - - /* Write I/O log file entry. */ - if (iolog_write(iol, buf, len, &errstr) == -1) - goto done; - - /* Write timing file entry. */ sudo_timespecsub(&now, &last_time, &delay); - len = (unsigned int)snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %u\n", - event, (long long)delay.tv_sec, delay.tv_nsec, len); - if (len >= sizeof(tbuf)) { - /* Not actually possible due to the size of tbuf[]. */ - errstr = strerror(EOVERFLOW); - goto done; - } - if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, &errstr) == -1) - goto done; - /* Success. */ - ret = 1; + if (iolog_remote) + ret = sudoers_io_log_remote(event, buf, len, &delay, &errstr); + else + ret = sudoers_io_log_local(event, buf, len, &delay, &errstr); -done: last_time.tv_sec = now.tv_sec; last_time.tv_nsec = now.tv_nsec; @@ -674,39 +882,87 @@ bad: static int sudoers_io_log_stdin(const char *buf, unsigned int len) { - return sudoers_io_log(&iolog_files[IOFD_STDIN], buf, len, IO_EVENT_STDIN); + return sudoers_io_log(buf, len, IO_EVENT_STDIN); } static int sudoers_io_log_stdout(const char *buf, unsigned int len) { - return sudoers_io_log(&iolog_files[IOFD_STDOUT], buf, len, IO_EVENT_STDOUT); + return sudoers_io_log(buf, len, IO_EVENT_STDOUT); } static int sudoers_io_log_stderr(const char *buf, unsigned int len) { - return sudoers_io_log(&iolog_files[IOFD_STDERR], buf, len, IO_EVENT_STDERR); + return sudoers_io_log(buf, len, IO_EVENT_STDERR); } static int sudoers_io_log_ttyin(const char *buf, unsigned int len) { - return sudoers_io_log(&iolog_files[IOFD_TTYIN], buf, len, IO_EVENT_TTYIN); + return sudoers_io_log(buf, len, IO_EVENT_TTYIN); } static int sudoers_io_log_ttyout(const char *buf, unsigned int len) { - return sudoers_io_log(&iolog_files[IOFD_TTYOUT], buf, len, IO_EVENT_TTYOUT); + return sudoers_io_log(buf, len, IO_EVENT_TTYOUT); +} + +static int +sudoers_io_change_winsize_local(unsigned int lines, unsigned int cols, + struct timespec *delay, const char **errstr) +{ + char tbuf[1024]; + int len, ret = -1; + debug_decl(sudoers_io_change_winsize_local, SUDOERS_DEBUG_PLUGIN) + + /* Write window change event to the timing file. */ + len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %u %u\n", + IO_EVENT_WINSIZE, (long long)delay->tv_sec, delay->tv_nsec, + lines, cols); + if (len < 0 || len >= ssizeof(tbuf)) { + /* Not actually possible due to the size of tbuf[]. */ + *errstr = strerror(EOVERFLOW); + goto done; + } + if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, errstr) == -1) + goto done; + + /* Success. */ + ret = 1; + +done: + debug_return_int(ret); +} + +static int +sudoers_io_change_winsize_remote(unsigned int lines, unsigned int cols, + struct timespec *delay, const char **errstr) +{ + int ret = -1; + debug_decl(sudoers_io_change_winsize_remote, SUDOERS_DEBUG_PLUGIN) + + if (client_closure.disabled) + debug_return_int(1); + + /* Track elapsed time for comparison with commit points. */ + sudo_timespecadd(delay, &client_closure.elapsed, &client_closure.elapsed); + + if (fmt_winsize(&client_closure, lines, cols, delay)) { + ret = client_closure.write_ev->add(client_closure.write_ev, + &iolog_details.server_timeout); + if (ret == -1) + sudo_warn(U_("unable to add event to queue")); + } + + debug_return_int(ret); } static int sudoers_io_change_winsize(unsigned int lines, unsigned int cols) { struct timespec now, delay; - unsigned int len; - char tbuf[1024]; const char *errstr = NULL; int ret = -1; debug_decl(sudoers_io_change_winsize, SUDOERS_DEBUG_PLUGIN) @@ -717,23 +973,13 @@ sudoers_io_change_winsize(unsigned int lines, unsigned int cols) errstr = strerror(errno); goto bad; } - - /* Write window change event to the timing file. */ sudo_timespecsub(&now, &last_time, &delay); - len = (unsigned int)snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %u %u\n", - IO_EVENT_WINSIZE, (long long)delay.tv_sec, delay.tv_nsec, lines, cols); - if (len >= sizeof(tbuf)) { - /* Not actually possible due to the size of tbuf[]. */ - errstr = strerror(EOVERFLOW); - goto done; - } - if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, &errstr) == -1) - goto done; - /* Success. */ - ret = 1; + if (iolog_remote) + ret = sudoers_io_change_winsize_remote(lines, cols, &delay, &errstr); + else + ret = sudoers_io_change_winsize_local(lines, cols, &delay, &errstr); -done: last_time.tv_sec = now.tv_sec; last_time.tv_nsec = now.tv_nsec; @@ -754,13 +1000,61 @@ bad: debug_return_int(ret); } +static int +sudoers_io_suspend_local(const char *signame, struct timespec *delay, + const char **errstr) +{ + unsigned int len; + char tbuf[1024]; + int ret = -1; + debug_decl(sudoers_io_suspend_local, SUDOERS_DEBUG_PLUGIN) + + /* Write suspend event to the timing file. */ + len = (unsigned int)snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %s\n", + IO_EVENT_SUSPEND, (long long)delay->tv_sec, delay->tv_nsec, signame); + if (len >= sizeof(tbuf)) { + /* Not actually possible due to the size of tbuf[]. */ + *errstr = strerror(EOVERFLOW); + goto done; + } + if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, errstr) == -1) + goto done; + + /* Success. */ + ret = 1; + +done: + debug_return_int(ret); +} + +static int +sudoers_io_suspend_remote(const char *signame, struct timespec *delay, + const char **errstr) +{ + int ret = -1; + debug_decl(sudoers_io_suspend_remote, SUDOERS_DEBUG_PLUGIN) + + if (client_closure.disabled) + debug_return_int(1); + + /* Track elapsed time for comparison with commit points. */ + sudo_timespecadd(delay, &client_closure.elapsed, &client_closure.elapsed); + + if (fmt_suspend(&client_closure, signame, delay)) { + ret = client_closure.write_ev->add(client_closure.write_ev, + &iolog_details.server_timeout); + if (ret == -1) + sudo_warn(U_("unable to add event to queue")); + } + + debug_return_int(ret); +} + static int sudoers_io_suspend(int signo) { struct timespec now, delay; - unsigned int len; char signame[SIG2STR_MAX]; - char tbuf[1024]; const char *errstr = NULL; int ret = -1; debug_decl(sudoers_io_suspend, SUDOERS_DEBUG_PLUGIN) @@ -777,23 +1071,14 @@ sudoers_io_suspend(int signo) errstr = strerror(errno); goto bad; } + sudo_timespecsub(&now, &last_time, &delay); /* Write suspend event to the timing file. */ - sudo_timespecsub(&now, &last_time, &delay); - len = (unsigned int)snprintf(tbuf, sizeof(tbuf), "%d %lld.%09ld %s\n", - IO_EVENT_SUSPEND, (long long)delay.tv_sec, delay.tv_nsec, signame); - if (len >= sizeof(tbuf)) { - /* Not actually possible due to the size of tbuf[]. */ - errstr = strerror(EOVERFLOW); - goto done; - } - if (iolog_write(&iolog_files[IOFD_TIMING], tbuf, len, &errstr) == -1) - goto done; + if (iolog_remote) + ret = sudoers_io_suspend_remote(signame, &delay, &errstr); + else + ret = sudoers_io_suspend_local(signame, &delay, &errstr); - /* Success. */ - ret = 1; - -done: last_time.tv_sec = now.tv_sec; last_time.tv_nsec = now.tv_nsec; diff --git a/plugins/sudoers/iolog_client.c b/plugins/sudoers/iolog_client.c new file mode 100644 index 000000000..1619a13dd --- /dev/null +++ b/plugins/sudoers/iolog_client.c @@ -0,0 +1,1159 @@ +/* + * Copyright (c) 2019 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 "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include +#elif defined(HAVE_INTTYPES_H) +# include +#endif +#include +#include +#include +#include +#include +#include +#include + +#include "sudoers.h" +#include "sudo_event.h" +#include "iolog_plugin.h" + +#ifndef HAVE_GETADDRINFO +# include "compat/getaddrinfo.h" +#endif + +static void +connect_cb(int sock, int what, void *v) +{ + int optval, ret, *errnump = v; + socklen_t optlen = sizeof(optval); + debug_decl(connect_cb, SUDOERS_DEBUG_UTIL) + + if (what == SUDO_PLUGIN_EV_TIMEOUT) { + *errnump = ETIMEDOUT; + } else { + ret = getsockopt(sock, SOL_SOCKET, SO_ERROR, &optval, &optlen); + *errnump = ret == 0 ? optval : errno; + } + + debug_return; +} + +/* + * Like connect(2) but with a timeout. + */ +static int +timed_connect(int sock, const struct sockaddr *addr, socklen_t addrlen, + struct timespec *timo) +{ + struct sudo_event_base *evbase = NULL; + struct sudo_event *connect_event = NULL; + int ret, errnum = 0; + debug_decl(timed_connect, SUDOERS_DEBUG_UTIL) + + ret = connect(sock, addr, addrlen); + if (ret == -1 && errno == EINPROGRESS) { + evbase = sudo_ev_base_alloc(); + connect_event = sudo_ev_alloc(sock, SUDO_PLUGIN_EV_WRITE, connect_cb, + &errnum); + if (evbase == NULL || connect_event == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + if (sudo_ev_add(evbase, connect_event, timo, false) == -1) { + sudo_warnx(U_("unable to add event to queue")); + goto done; + } + if (sudo_ev_dispatch(evbase) == -1) { + sudo_warn(U_("error in event loop")); + goto done; + } + if (errnum == 0) + ret = 0; + else + errno = errnum; + } + +done: + sudo_ev_base_free(evbase); + sudo_ev_free(connect_event); + + debug_return_int(ret); +} + +/* + * Connect to specified host:port + * If host has multiple addresses, the first one that connects is used. + * Returns open socket or -1 on error. + */ +static int +connect_server(const char *host, const char *port, struct timespec *timo, + const char **reason) +{ + struct addrinfo hints, *res, *res0; + const char *cause = NULL; + int error, sock = -1; + debug_decl(connect_server, SUDOERS_DEBUG_UTIL) + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(host, port, &hints, &res0); + if (error != 0) { + sudo_warnx(U_("unable to look up %s:%s: %s"), host, port, + gai_strerror(error)); + debug_return_int(-1); + } + + for (res = res0; res; res = res->ai_next) { + int flags, save_errno; + + sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock == -1) { + cause = "socket"; + continue; + } + flags = fcntl(sock, F_GETFL, 0); + if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { + cause = "fcntl(O_NONBLOCK)"; + save_errno = errno; + close(sock); + errno = save_errno; + sock = -1; + continue; + } + if (timed_connect(sock, res->ai_addr, res->ai_addrlen, timo) == -1) { + cause = "connect"; + save_errno = errno; + close(sock); + errno = save_errno; + sock = -1; + continue; + } + break; /* success */ + } + freeaddrinfo(res0); + + if (sock == -1) + *reason = cause; + + debug_return_int(sock); +} + +/* + * Connect to the first server in the list. + * Returns a socket with O_NONBLOCK and close-on-exec flags set. + */ +int +log_server_connect(struct sudoers_str_list *servers, struct timespec *timo) +{ + struct sudoers_string *server; + char *copy, *host, *port; + const char *cause = NULL; + int sock = -1; + debug_decl(restore_nproc, SUDOERS_DEBUG_UTIL) + + STAILQ_FOREACH(server, servers, entries) { + copy = strdup(server->str); + if (!sudo_parse_host_port(copy, &host, &port, DEFAULT_PORT_STR)) { + free(copy); + continue; + } + sock = connect_server(host, port, timo, &cause); + free(copy); + if (sock != -1) { + int flags = fcntl(sock, F_GETFL, 0); + if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1 || + fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { + close(sock); + sock = -1; + } + break; + } + } + if (sock == -1 && cause != NULL) + sudo_warn("%s", cause); + + debug_return_int(sock); +} + +/* + * Free client closure and contents and initialize to unused state as + * per CLIENT_CLOSURE_INITIALIZER. Log details are not freed. + */ +void +client_closure_free(struct client_closure *closure) +{ + struct connection_buffer *buf; + debug_decl(client_closure_free, SUDOERS_DEBUG_UTIL) + + if (closure->sock != -1) { + close(closure->sock); + closure->sock = -1; + } + closure->state = ERROR; + while ((buf = TAILQ_FIRST(&closure->write_bufs)) != NULL) { + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + free(buf->data); + free(buf); + } + while ((buf = TAILQ_FIRST(&closure->free_bufs)) != NULL) { + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + free(buf->data); + free(buf); + } + if (closure->read_ev != NULL) { + closure->read_ev->free(closure->read_ev); + closure->read_ev = NULL; + } + if (closure->write_ev != NULL) { + closure->write_ev->free(closure->write_ev); + closure->write_ev = NULL; + } + free(closure->read_buf.data); + memset(&closure->read_buf, 0, sizeof(closure->read_buf)); + closure->log_details = NULL; + memset(&closure->start_time, 0, sizeof(closure->start_time)); + memset(&closure->elapsed, 0, sizeof(closure->elapsed)); + memset(&closure->committed, 0, sizeof(closure->committed)); + free(closure->iolog_id); + closure->iolog_id = NULL; + + debug_return; +} + +static struct connection_buffer * +get_free_buf(struct client_closure *closure) +{ + struct connection_buffer *buf; + debug_decl(get_free_buf, SUDOERS_DEBUG_UTIL) + + buf = TAILQ_FIRST(&closure->free_bufs); + if (buf != NULL) + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + else + buf = calloc(1, sizeof(*buf)); + + debug_return_ptr(buf); +} + +/* + * Format a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_client_message(struct client_closure *closure, ClientMessage *msg) +{ + struct connection_buffer *buf; + uint32_t msg_len; + bool ret = false; + size_t len; + debug_decl(fmt_client_message, SUDOERS_DEBUG_UTIL) + + if ((buf = get_free_buf(closure)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + + len = client_message__get_packed_size(msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("client message too large: %zu\n"), len); + goto done; + } + /* Wire message size is used for length encoding, precedes message. */ + msg_len = htonl((uint32_t)len); + len += sizeof(msg_len); + + /* Resize buffer as needed. */ + if (len > buf->size) { + free(buf->data); + buf->size = sudo_pow2_roundup(len); + if ((buf->data = malloc(buf->size)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + buf->size = 0; + goto done; + } + } + + memcpy(buf->data, &msg_len, sizeof(msg_len)); + client_message__pack(msg, buf->data + sizeof(msg_len)); + buf->len = len; + TAILQ_INSERT_TAIL(&closure->write_bufs, buf, entries); + buf = NULL; + + ret = true; + +done: + if (buf != NULL) { + free(buf->data); + free(buf); + } + debug_return_bool(ret); +} + +/* + * Build and format an AcceptMessage wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_accept_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + AcceptMessage accept_msg = ACCEPT_MESSAGE__INIT; + TimeSpec ts = TIME_SPEC__INIT; + InfoMessage__StringList runargv = INFO_MESSAGE__STRING_LIST__INIT; + InfoMessage__StringList submitenv = INFO_MESSAGE__STRING_LIST__INIT; + struct iolog_details *details = closure->log_details; + struct timespec now; + bool ret = false; + size_t n; + debug_decl(fmt_accept_message, SUDOERS_DEBUG_UTIL) + + /* + * Fill in AcceptMessage and add it to ClientMessage. + */ + if (sudo_gettime_real(&now)) { + sudo_warn("%s", U_("unable to get time of day")); + debug_return_bool(false); + } + ts.tv_sec = now.tv_sec; + ts.tv_nsec = now.tv_nsec; + accept_msg.submit_time = &ts; + + /* Client will send IoBuffer messages. */ + accept_msg.expect_iobufs = true; + + /* Convert NULL-terminated vectors to StringList. */ + runargv.strings = (char **)details->argv; + runargv.n_strings = details->argc; + submitenv.strings = (char **)details->user_env; + while (submitenv.strings[submitenv.n_strings] != NULL) + submitenv.n_strings++; + + /* XXX - realloc as needed instead of preallocating */ + accept_msg.n_info_msgs = 22; + accept_msg.info_msgs = calloc(accept_msg.n_info_msgs, sizeof(InfoMessage *)); + if (accept_msg.info_msgs == NULL) { + accept_msg.n_info_msgs = 0; + goto done; + } + for (n = 0; n < accept_msg.n_info_msgs; n++) { + accept_msg.info_msgs[n] = malloc(sizeof(InfoMessage)); + if (accept_msg.info_msgs[n] == NULL) { + accept_msg.n_info_msgs = n; + goto done; + } + info_message__init(accept_msg.info_msgs[n]); + } + + /* Fill in info_msgs */ + n = 0; + + /* clientargv (not supported) */ + /* clientpid */ + /* clientppid */ + /* clientsid */ + + accept_msg.info_msgs[n]->key = "columns"; + accept_msg.info_msgs[n]->numval = details->cols; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + accept_msg.info_msgs[n]->key = "command"; + accept_msg.info_msgs[n]->strval = (char *)details->command; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + accept_msg.info_msgs[n]->key = "lines"; + accept_msg.info_msgs[n]->numval = details->lines; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + accept_msg.info_msgs[n]->key = "runargv"; + accept_msg.info_msgs[n]->strlistval = &runargv; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL; + n++; + + accept_msg.info_msgs[n]->key = "submitenv"; + accept_msg.info_msgs[n]->strlistval = &submitenv; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL; + n++; + + if (details->runas_gr!= NULL) { + accept_msg.info_msgs[n]->key = "rungid"; + accept_msg.info_msgs[n]->numval = details->runas_gr->gr_gid; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + accept_msg.info_msgs[n]->key = "rungroup"; + accept_msg.info_msgs[n]->strval = details->runas_gr->gr_name; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + } + + /* TODO - rungids */ + /* TODO - rungroups */ + + accept_msg.info_msgs[n]->key = "runuid"; + accept_msg.info_msgs[n]->numval = details->runas_pw->pw_uid; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + accept_msg.info_msgs[n]->key = "runuser"; + accept_msg.info_msgs[n]->strval = details->runas_pw->pw_name; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + accept_msg.info_msgs[n]->key = "submitcwd"; + accept_msg.info_msgs[n]->strval = (char *)details->cwd; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + /* TODO - submitenv */ + /* TODO - submitgid */ + /* TODO - submitgids */ + /* TODO - submitgroup */ + /* TODO - submitgroups */ + + accept_msg.info_msgs[n]->key = "submithost"; + accept_msg.info_msgs[n]->strval = (char *)details->host; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + /* TODO - submituid */ + + accept_msg.info_msgs[n]->key = "submituser"; + accept_msg.info_msgs[n]->strval = (char *)details->user; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + accept_msg.info_msgs[n]->key = "ttyname"; + accept_msg.info_msgs[n]->strval = (char *)details->tty; + accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + /* Update n_info_msgs. */ + accept_msg.n_info_msgs = n; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending AcceptMessage, array length %zu", __func__, n); + + /* Schedule ClientMessage */ + client_msg.accept_msg = &accept_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_ACCEPT_MSG; + ret = fmt_client_message(closure, &client_msg); + +done: + for (n = 0; n < accept_msg.n_info_msgs; n++) + free(accept_msg.info_msgs[n]); + free(accept_msg.info_msgs); + + debug_return_bool(ret); +} + +#ifdef notyet +/* + * Build and format a RestartMessage wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_restart_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + RestartMessage restart_msg = RESTART_MESSAGE__INIT; + TimeSpec tv = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_restart_message, SUDOERS_DEBUG_UTIL) + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending RestartMessage, [%lld, %ld]", __func__, + (long long)closure->restart->tv_sec, closure->restart->tv_nsec); + + tv.tv_sec = closure->restart->tv_sec; + tv.tv_nsec = closure->restart->tv_nsec; + restart_msg.resume_point = &tv; + restart_msg.log_id = (char *)closure->iolog_id; + + /* Schedule ClientMessage */ + client_msg.restart_msg = &restart_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG; + ret = fmt_client_message(closure, &client_msg); + + debug_return_bool(ret); +} +#endif + +/* + * Build and format an ExitMessage wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_exit_message(struct client_closure *closure, int exit_status, int error) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ExitMessage exit_msg = EXIT_MESSAGE__INIT; + TimeSpec ts = TIME_SPEC__INIT; + char signame[SIG2STR_MAX]; + bool ret = false; + struct timespec run_time; + debug_decl(fmt_exit_message, SUDOERS_DEBUG_UTIL) + + if (sudo_gettime_awake(&run_time) == -1) { + sudo_warn("%s", U_("unable to get time of day")); + goto done; + } + sudo_timespecsub(&run_time, &closure->start_time, &run_time); + + ts.tv_sec = run_time.tv_sec; + ts.tv_nsec = run_time.tv_nsec; + exit_msg.run_time = &ts; + + if (error != 0) { + /* Error executing the command. */ + exit_msg.error = strerror(error); + } else { + if (WIFEXITED(exit_status)) { + exit_msg.exit_value = WEXITSTATUS(exit_status); + } else if (WIFSIGNALED(exit_status)) { + int signo = WTERMSIG(exit_status); + if (signo <= 0 || sig2str(signo, signame) == -1) { + sudo_warnx(U_("%s: internal error, invalid signal %d"), + __func__, signo); + goto done; + } + exit_msg.signal = signame; + if (WCOREDUMP(exit_status)) + exit_msg.dumped_core = true; + exit_msg.exit_value = WTERMSIG(exit_status) | 128; + } else { + sudo_warnx(U_("%s: internal error, invalid exit status %d"), + __func__, exit_status); + goto done; + } + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending ExitMessage, exitval %d, error %s, signal %s, coredump %s", + __func__, exit_msg.exit_value, exit_msg.error ? exit_msg.error : "", + exit_msg.signal ? exit_msg.signal : "", + exit_msg.dumped_core ? "yes" : "no"); + + /* Send ClientMessage */ + client_msg.exit_msg = &exit_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_EXIT_MSG; + if (!fmt_client_message(closure, &client_msg)) + goto done; + + closure->state = SEND_EXIT; + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format an IoBuffer wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_io_buf(struct client_closure *closure, int type, const char *buf, + unsigned int len, struct timespec *delay) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + IoBuffer iobuf_msg = IO_BUFFER__INIT; + TimeSpec ts = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_io_buf, SUDOERS_DEBUG_UTIL) + + /* Fill in IoBuffer. */ + ts.tv_sec = delay->tv_sec; + ts.tv_nsec = delay->tv_nsec; + iobuf_msg.delay = &ts; + iobuf_msg.data.data = (void *)buf; + iobuf_msg.data.len = len; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending IoBuffer length %zu, type %d, size %zu", __func__, + iobuf_msg.data.len, type, io_buffer__get_packed_size(&iobuf_msg)); + + /* Schedule ClientMessage, it doesn't matter which IoBuffer we set. */ + client_msg.ttyout_buf = &iobuf_msg; + client_msg.type_case = type; + if (!fmt_client_message(closure, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format a ChangeWindowSize message wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_winsize(struct client_closure *closure, unsigned int lines, + unsigned int cols, struct timespec *delay) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ChangeWindowSize winsize_msg = CHANGE_WINDOW_SIZE__INIT; + TimeSpec ts = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_winsize, SUDOERS_DEBUG_UTIL) + + /* Fill in ChangeWindowSize message. */ + ts.tv_sec = delay->tv_sec; + ts.tv_nsec = delay->tv_nsec; + winsize_msg.delay = &ts; + winsize_msg.rows = lines; + winsize_msg.cols = cols; + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ChangeWindowSize, %dx%d", + __func__, winsize_msg.rows, winsize_msg.cols); + + /* Send ClientMessage */ + client_msg.winsize_event = &winsize_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_WINSIZE_EVENT; + if (!fmt_client_message(closure, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format a CommandSuspend message wrapped in a ClientMessage. + * Appends the wire format message to the closure's write queue. + * Returns true on success, false on failure. + */ +bool +fmt_suspend(struct client_closure *closure, const char *signame, struct timespec *delay) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + CommandSuspend suspend_msg = COMMAND_SUSPEND__INIT; + TimeSpec ts = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_suspend, SUDOERS_DEBUG_UTIL) + + /* Fill in CommandSuspend message. */ + ts.tv_sec = delay->tv_sec; + ts.tv_nsec = delay->tv_nsec; + suspend_msg.delay = &ts; + suspend_msg.signal = (char *)signame; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending CommandSuspend, SIG%s", __func__, suspend_msg.signal); + + /* Send ClientMessage */ + client_msg.suspend_event = &suspend_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_SUSPEND_EVENT; + if (!fmt_client_message(closure, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Additional work to do after a ClientMessage was sent to the server. + * Advances state and formats the next ClientMessage (if any). + * XXX - better name + */ +static bool +client_message_completion(struct client_closure *closure) +{ + debug_decl(client_message_completion, SUDOERS_DEBUG_UTIL) + + switch (closure->state) { + case SEND_ACCEPT: + case SEND_RESTART: + closure->state = SEND_IO; + break; + case SEND_IO: + /* Arbitrary number of I/O log buffers, no state change. */ + break; + case SEND_EXIT: + /* Done writing, just waiting for final commit point. */ + closure->write_ev->del(closure->write_ev); + closure->state = CLOSING; + + /* Enable timeout while waiting for final commit point. */ + if (closure->read_ev->add(closure->read_ev, + &closure->log_details->server_timeout) == -1) { + sudo_warn(U_("unable to add event to queue")); + debug_return_bool(false); + } + break; + default: + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Respond to a ServerHello message from the server. + * Returns true on success, false on error. + */ +static bool +handle_server_hello(ServerHello *msg, struct client_closure *closure) +{ + size_t n; + debug_decl(handle_server_hello, SUDOERS_DEBUG_UTIL) + + if (closure->state != RECV_HELLO) { + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + + /* Sanity check ServerHello message. */ + if (msg->server_id == NULL || msg->server_id[0] == '\0') { + sudo_warnx("%s", U_("invalid ServerHello")); + debug_return_bool(false); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: server ID: %s", + __func__, msg->server_id); + /* TODO: handle redirect */ + if (msg->redirect != NULL && msg->redirect[0] != '\0') { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: redirect: %s", + __func__, msg->redirect); + } + for (n = 0; n < msg->n_servers; n++) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: server %zu: %s", + __func__, n + 1, msg->servers[n]); + } + + /* + * Disable timeout for read event after have server hello. + * Other server messages may happen at arbitrary times. + */ + if (closure->read_ev->add(closure->read_ev, NULL) == -1) { + sudo_warn(U_("unable to add event to queue")); + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Respond to a CommitPoint message from the server. + * Returns true on success, false on error. + */ +static bool +handle_commit_point(TimeSpec *commit_point, struct client_closure *closure) +{ + debug_decl(handle_commit_point, SUDOERS_DEBUG_UTIL) + + /* Only valid after we have sent an IO buffer. */ + if (closure->state < SEND_IO) { + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: commit point: [%lld, %d]", + __func__, (long long)commit_point->tv_sec, commit_point->tv_nsec); + closure->committed.tv_sec = commit_point->tv_sec; + closure->committed.tv_nsec = commit_point->tv_nsec; + + if (closure->state == CLOSING) { + if (sudo_timespeccmp(&closure->elapsed, &closure->committed, ==)) { + /* Last commit point received, exit event loop. */ + closure->state = FINISHED; + closure->read_ev->del(closure->read_ev); + } + } + + debug_return_bool(true); +} + +/* + * Respond to a LogId message from the server. + * Always returns true. + */ +static bool +handle_log_id(char *id, struct client_closure *closure) +{ + debug_decl(handle_log_id, SUDOERS_DEBUG_UTIL) + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: remote log ID: %s", __func__, id); + if ((closure->iolog_id = strdup(id)) == NULL) + sudo_fatal(NULL); + debug_return_bool(true); +} + +/* + * Respond to a ServerError message from the server. + * Always returns false. + */ +static bool +handle_server_error(char *errmsg, struct client_closure *closure) +{ + debug_decl(handle_server_error, SUDOERS_DEBUG_UTIL) + + sudo_warnx(U_("error message received from server: %s"), errmsg); + debug_return_bool(false); +} + +/* + * Respond to a ServerAbort message from the server. + * Always returns false. + */ +static bool +handle_server_abort(char *errmsg, struct client_closure *closure) +{ + debug_decl(handle_server_abort, SUDOERS_DEBUG_UTIL) + + sudo_warnx(U_("abort message received from server: %s"), errmsg); + debug_return_bool(false); +} + +/* + * Respond to a ServerMessage from the server. + * Returns true on success, false on error. + */ +static bool +handle_server_message(uint8_t *buf, size_t len, + struct client_closure *closure) +{ + ServerMessage *msg; + bool ret = false; + debug_decl(handle_server_message, SUDOERS_DEBUG_UTIL) + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: unpacking ServerMessage", __func__); + msg = server_message__unpack(NULL, len, buf); + if (msg == NULL) { + sudo_warnx("%s", U_("unable to unpack ServerMessage")); + debug_return_bool(false); + } + + switch (msg->type_case) { + case SERVER_MESSAGE__TYPE_HELLO: + if (handle_server_hello(msg->hello, closure)) { + /* Format and schedule accept message. */ + closure->state = SEND_ACCEPT; + if ((ret = fmt_accept_message(closure))) { + if (closure->write_ev->add(closure->write_ev, + &closure->log_details->server_timeout) == -1) { + sudo_warn(U_("unable to add event to queue")); + ret = false; + } + } + } + break; + case SERVER_MESSAGE__TYPE_COMMIT_POINT: + ret = handle_commit_point(msg->commit_point, closure); + break; + case SERVER_MESSAGE__TYPE_LOG_ID: + ret = handle_log_id(msg->log_id, closure); + break; + case SERVER_MESSAGE__TYPE_ERROR: + ret = handle_server_error(msg->error, closure); + closure->state = ERROR; + break; + case SERVER_MESSAGE__TYPE_ABORT: + ret = handle_server_abort(msg->abort, closure); + closure->state = ERROR; + break; + default: + sudo_warnx(U_("%s: unexpected type_case value %d"), + __func__, msg->type_case); + break; + } + + server_message__free_unpacked(msg, NULL); + debug_return_bool(ret); +} + +/* + * Expand buf as needed or just reset it. + * XXX - share with logsrvd/sendlog + */ +static bool +expand_buf(struct connection_buffer *buf, unsigned int needed) +{ + void *newdata; + debug_decl(expand_buf, SUDO_DEBUG_UTIL) + + if (buf->size < needed) { + /* Expand buffer. */ + needed = sudo_pow2_roundup(needed); + if ((newdata = malloc(needed)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + if (buf->off > 0) + memcpy(newdata, buf->data + buf->off, buf->len - buf->off); + free(buf->data); + buf->data = newdata; + buf->size = needed; + } else { + /* Just reset existing buffer. */ + if (buf->off > 0) { + memmove(buf->data, buf->data + buf->off, + buf->len - buf->off); + } + } + buf->len -= buf->off; + buf->off = 0; + + debug_return_bool(true); +} + +/* + * Read and unpack a ServerMessage (read callback). + */ +static void +server_msg_cb(int fd, int what, void *v) +{ + struct client_closure *closure = v; + struct connection_buffer *buf = &closure->read_buf; + ssize_t nread; + uint32_t msg_len; + debug_decl(server_msg_cb, SUDOERS_DEBUG_UTIL) + + if (what == SUDO_PLUGIN_EV_TIMEOUT) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: timed out reading from server", + __func__); + goto bad; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage", __func__); + + nread = recv(fd, buf->data + buf->len, buf->size - buf->len, 0); + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from server", + __func__, nread); + switch (nread) { + case -1: + if (errno == EAGAIN) + debug_return; + sudo_warn("recv"); + goto bad; + case 0: + sudo_warnx("%s", U_("lost connection to log server")); + goto bad; + default: + break; + } + buf->len += nread; + + while (buf->len - buf->off >= sizeof(msg_len)) { + /* Read wire message size (uint32_t in network byte order). */ + memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len)); + msg_len = ntohl(msg_len); + + if (msg_len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("server message too large: %u\n"), msg_len); + goto bad; + } + + if (msg_len + sizeof(msg_len) > buf->len - buf->off) { + /* Incomplete message, we'll read the rest next time. */ + if (!expand_buf(buf, msg_len + sizeof(msg_len))) + goto bad; + debug_return; + } + + /* Parse ServerMessage, could be zero bytes. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: parsing ServerMessage, size %u", __func__, msg_len); + buf->off += sizeof(msg_len); + if (!handle_server_message(buf->data + buf->off, msg_len, closure)) + goto bad; + buf->off += msg_len; + } + buf->len -= buf->off; + buf->off = 0; + debug_return; +bad: + if (closure->log_details->ignore_iolog_errors) { + /* Disable plugin, the command continues. */ + closure->disabled = true; + closure->read_ev->del(closure->read_ev); + } else { + /* Break out of sudo event loop and kill the command. */ + closure->read_ev->loopbreak(closure->read_ev); + } + debug_return; +} + +/* + * Send a ClientMessage to the server (write callback). + */ +static void +client_msg_cb(int fd, int what, void *v) +{ + struct client_closure *closure = v; + struct connection_buffer *buf; + ssize_t nwritten; + debug_decl(client_msg_cb, SUDOERS_DEBUG_UTIL) + + if (what == SUDO_PLUGIN_EV_TIMEOUT) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: timed out writiing to server", + __func__); + goto bad; + } + + if ((buf = TAILQ_FIRST(&closure->write_bufs)) == NULL) { + sudo_warn("%s", U_("missing write buffer")); + goto bad; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending %u bytes to server", __func__, buf->len - buf->off); + + nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0); + if (nwritten == -1) { + sudo_warn("send"); + goto bad; + } + buf->off += nwritten; + + if (buf->off == buf->len) { + /* sent entire message, move buf to free list */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: finished sending %u bytes to server", __func__, buf->len); + buf->off = 0; + buf->len = 0; + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); + if (TAILQ_EMPTY(&closure->write_bufs)) { + /* Write queue empty, check for state change. */ + closure->write_ev->del(closure->write_ev); + if (!client_message_completion(closure)) + goto bad; + } + } else { + /* not done yet */ + TAILQ_INSERT_HEAD(&closure->write_bufs, buf, entries); + } + debug_return; + +bad: + if (closure->log_details->ignore_iolog_errors) { + /* Disable plugin, the command continues. */ + closure->disabled = true; + closure->write_ev->del(closure->write_ev); + } else { + /* Break out of sudo event loop and kill the command. */ + closure->write_ev->loopbreak(closure->write_ev); + } + debug_return; +} + +/* + * Allocate and initialize a new client closure + */ +bool +client_closure_fill(struct client_closure *closure, int sock, + struct iolog_details *details, struct io_plugin *sudoers_io) +{ + debug_decl(client_closure_alloc, SUDOERS_DEBUG_UTIL) + + closure->sock = -1; + closure->state = RECV_HELLO; + + closure->read_buf.size = 64 * 1024; + closure->read_buf.data = malloc(closure->read_buf.size); + if (closure->read_buf.data == NULL) + goto oom; + + TAILQ_INIT(&closure->write_bufs); + TAILQ_INIT(&closure->free_bufs); + + if ((closure->read_ev = sudoers_io->event_alloc()) == NULL) + goto oom; + + if ((closure->write_ev = sudoers_io->event_alloc()) == NULL) + goto oom; + + if (closure->read_ev->set(closure->read_ev, sock, + SUDO_PLUGIN_EV_READ|SUDO_PLUGIN_EV_PERSIST, + server_msg_cb, closure) == -1) + goto oom; + + if (closure->write_ev->set(closure->write_ev, sock, + SUDO_PLUGIN_EV_WRITE|SUDO_PLUGIN_EV_PERSIST, + client_msg_cb, closure) == -1) + goto oom; + + closure->log_details = details; + + /* Store sock last to avoid double-close in parent on error. */ + closure->sock = sock; + + debug_return_ptr(closure); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + client_closure_free(closure); + debug_return_ptr(NULL); +} + +/* + * Custom event loop called from the plugin close function. + * We cannot use the main sudo event loop as it has already exited. + */ +bool +client_loop(struct client_closure *closure) +{ + struct sudo_event_base *evbase; + debug_decl(client_loop, SUDOERS_DEBUG_UTIL) + + /* Create private event base and reparent the read/write events. */ + if ((evbase = sudo_ev_base_alloc()) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + closure->read_ev->setbase(closure->read_ev, evbase); + closure->write_ev->setbase(closure->read_ev, evbase); + + /* Loop until queues are flushed and final commit point received. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "flushing buffers and waiting for final commit point"); + sudo_ev_dispatch(evbase); + sudo_ev_base_free(evbase); + + debug_return_bool(true); +} diff --git a/plugins/sudoers/iolog_plugin.h b/plugins/sudoers/iolog_plugin.h new file mode 100644 index 000000000..cf0e6b975 --- /dev/null +++ b/plugins/sudoers/iolog_plugin.h @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019 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. + */ + +#ifndef SUDOERS_IOLOG_CLIENT_H +#define SUDOERS_IOLOG_CLIENT_H + +#include "log_server.pb-c.h" +#include "strlist.h" + +#if PROTOBUF_C_VERSION_NUMBER < 1003000 +# error protobuf-c version 1.30 or higher required +#endif + +/* Default port to listen on */ +#define DEFAULT_PORT_STR "30344" + +/* Maximum message size (2Mb) */ +#define MESSAGE_SIZE_MAX (2 * 1024 * 1024) + +/* TODO - share with logsrvd/sendlog */ +struct connection_buffer { + TAILQ_ENTRY(connection_buffer) entries; + uint8_t *data; + unsigned int size; + unsigned int len; + unsigned int off; +}; +TAILQ_HEAD(connection_buffer_list, connection_buffer); + +/* XXX - remove dependency on sudoers.h? */ +#undef runas_pw +#undef runas_gr + +struct iolog_details { + const char *cwd; + const char *host; + const char *tty; + const char *user; + const char *command; + const char *iolog_path; + struct passwd *runas_pw; + struct group *runas_gr; + char * const *argv; + char * const *user_env; + struct sudoers_str_list *log_servers; + struct timespec server_timeout; + int argc; + int lines; + int cols; + bool ignore_iolog_errors; +}; + +enum client_state { + ERROR, + RECV_HELLO, + SEND_RESTART, /* TODO: currently unimplemented */ + SEND_ACCEPT, + SEND_IO, + SEND_EXIT, + CLOSING, + FINISHED +}; + +/* Remote connection closure, non-zero fields must come first. */ +struct client_closure { + int sock; + enum client_state state; + bool disabled; + struct connection_buffer_list write_bufs; + struct connection_buffer_list free_bufs; + struct connection_buffer read_buf; + struct sudo_plugin_event *read_ev; + struct sudo_plugin_event *write_ev; + struct iolog_details *log_details; + struct timespec start_time; + struct timespec elapsed; + struct timespec committed; + char *iolog_id; +}; + +#define CLIENT_CLOSURE_INITIALIZER(_c) \ + { \ + -1, \ + ERROR, \ + false, \ + TAILQ_HEAD_INITIALIZER((_c).write_bufs), \ + TAILQ_HEAD_INITIALIZER((_c).free_bufs) \ + } + +/* iolog_client.c */ +bool client_closure_fill(struct client_closure *closure, int sock, struct iolog_details *details, struct io_plugin *sudoers_io); +bool client_loop(struct client_closure *closure); +bool fmt_accept_message(struct client_closure *closure); +bool fmt_client_message(struct client_closure *closure, ClientMessage *msg); +bool fmt_exit_message(struct client_closure *closure, int exit_status, int error); +bool fmt_io_buf(struct client_closure *closure, int type, const char *buf, unsigned int len, struct timespec *delay); +bool fmt_suspend(struct client_closure *closure, const char *signame, struct timespec *delay); +bool fmt_winsize(struct client_closure *closure, unsigned int lines, unsigned int cols, struct timespec *delay); +int log_server_connect(struct sudoers_str_list *servers, struct timespec *timo); +void client_closure_free(struct client_closure *closure); + +#endif /* SUDOERS_IOLOG_CLIENT_H */ diff --git a/plugins/sudoers/policy.c b/plugins/sudoers/policy.c index 8c2c827e9..757bebf49 100644 --- a/plugins/sudoers/policy.c +++ b/plugins/sudoers/policy.c @@ -506,6 +506,45 @@ bad: debug_return_int(MODE_ERROR); } +/* + * Convert struct list_members to a comma-separated string with + * the given variable name. + */ +static char * +serialize_list(const char *varname, struct list_members *members) +{ + struct list_member *lm, *next; + size_t len, result_size; + char *result; + debug_decl(serialize_list, SUDOERS_DEBUG_PLUGIN) + + result_size = strlen(varname) + 1; + SLIST_FOREACH(lm, members, entries) { + result_size += strlen(lm->value) + 1; + } + if ((result = malloc(result_size)) == NULL) + goto bad; + /* No need to check len for overflow here. */ + len = strlcpy(result, varname, result_size); + result[len++] = '='; + result[len] = '\0'; + SLIST_FOREACH_SAFE(lm, members, entries, next) { + len = strlcat(result, lm->value, result_size); + if (len + (next != NULL) >= result_size) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + goto bad; + } + if (next != NULL) { + result[len++] = ','; + result[len] = '\0'; + } + } + debug_return_str(result); +bad: + free(result); + debug_return_str(NULL); +} + /* * Setup the execution environment. * Builds up the command_info list and sets argv and envp. @@ -686,6 +725,15 @@ sudoers_policy_exec_setup(char *argv[], char *envp[], mode_t cmnd_umask, if ((command_info[info_len++] = sudo_new_key_val("iolog_group", def_iolog_group)) == NULL) goto oom; } + if (!SLIST_EMPTY(&def_log_server)) { + char *log_servers = serialize_list("log_servers", &def_log_server); + if (log_servers == NULL) + goto oom; + command_info[info_len++] = log_servers; + + if (asprintf(&command_info[info_len++], "log_server_timeout=%u", def_log_server_timeout) == -1) + goto oom; + } if (def_command_timeout > 0 || user_timeout > 0) { int timeout = user_timeout; if (timeout == 0 || def_command_timeout < timeout) diff --git a/src/exec_nopty.c b/src/exec_nopty.c index e2901f4e2..de02d5fbc 100644 --- a/src/exec_nopty.c +++ b/src/exec_nopty.c @@ -438,6 +438,7 @@ exec_nopty(struct command_details *details, struct command_status *cstat) sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely"); /* kill command */ terminate_command(ec.cmnd_pid, true); + ec.cmnd_pid = -1; } #ifdef HAVE_SELINUX diff --git a/src/exec_pty.c b/src/exec_pty.c index 0b19eeb73..8fe57950a 100644 --- a/src/exec_pty.c +++ b/src/exec_pty.c @@ -1581,7 +1581,12 @@ exec_pty(struct command_details *details, struct command_status *cstat) if (sudo_ev_got_break(ec.evbase)) { /* error from callback or monitor died */ sudo_debug_printf(SUDO_DEBUG_ERROR, "event loop exited prematurely"); - /* XXX - may need to terminate command if cmnd_pid != -1 */ + /* kill command */ + terminate_command(ec.cmnd_pid, true); + ec.cmnd_pid = -1; + /* TODO: need way to pass an error to the sudo front end */ + cstat->type = CMD_WSTATUS; + cstat->val = W_EXITCODE(1, SIGKILL); } /* Flush any remaining output, free I/O bufs and events, do logout. */ diff --git a/src/sudo.c b/src/sudo.c index 83efbb099..866095919 100644 --- a/src/sudo.c +++ b/src/sudo.c @@ -1479,6 +1479,7 @@ sudo_plugin_event_alloc(void) ev_int->public.fd = plugin_event_fd; ev_int->public.timeleft = plugin_event_timeleft; ev_int->public.setbase = plugin_event_setbase; + ev_int->public.loopbreak = plugin_event_loopbreak; ev_int->public.free = plugin_event_free; /* Clear private portion in case caller tries to use us uninitialized. */