/* * Copyright (c) 2011-2014 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include /* for howmany() on Linux */ #ifdef HAVE_SYS_SYSMACROS_H # include /* for howmany() on Solaris */ #endif #ifdef HAVE_SYS_SELECT_H # include #endif /* HAVE_SYS_SELECT_H */ #include #include #include #include #ifdef STDC_HEADERS # include # include #else # ifdef HAVE_STDLIB_H # include # endif #endif /* STDC_HEADERS */ #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif #ifdef HAVE_STRING_H # include #endif /* HAVE_STRING_H */ #ifdef HAVE_STRINGS_H # include #endif /* HAVE_STRINGS_H */ #ifdef HAVE_UNISTD_H # include #endif /* HAVE_UNISTD_H */ #include #include #include #include #define DEFAULT_TEXT_DOMAIN "sudo" #include "sudo_gettext.h" /* must be included before sudo_compat.h */ #include "sudo_compat.h" #include "sudo_alloc.h" #include "sudo_fatal.h" #include "sudo_plugin.h" #include "sudo_conf.h" #include "sudo_debug.h" #include "sudo_util.h" /* * The debug priorities and subsystems are currently hard-coded. * In the future we might consider allowing plugins to register their * own subsystems and provide direct access to the debugging API. */ /* Note: this must match the order in sudo_debug.h */ static const char *const sudo_debug_priorities[] = { "crit", "err", "warn", "notice", "diag", "info", "trace", "debug", NULL }; /* Note: this must match the order in sudo_debug.h */ /* XXX - remove sudoers-specific bits */ static const char *const sudo_debug_default_subsystems[] = { "main", "args", "exec", "pty", "utmp", "conv", "pcomm", "util", "netif", "audit", "edit", "selinux", "ldap", "match", "parser", "alias", "defaults", "auth", "env", "logging", "nss", "rbtree", "perms", "plugin", "hooks", "sssd", "event", NULL }; #define NUM_SUBSYSTEMS (sizeof(sudo_debug_default_subsystems) / sizeof(sudo_debug_default_subsystems[0]) - 1) /* * For multiple programs/plugins there is a per-program instance * and one or more outputs (files). */ struct sudo_debug_output { SLIST_ENTRY(sudo_debug_output) entries; char *filename; int *settings; int fd; }; SLIST_HEAD(sudo_debug_output_list, sudo_debug_output); struct sudo_debug_instance { char *program; const char *const *subsystems; int num_subsystems; struct sudo_debug_output_list outputs; }; /* Support up to 10 instances. */ #define SUDO_DEBUG_INSTANCE_MAX 10 static struct sudo_debug_instance *sudo_debug_instances[SUDO_DEBUG_INSTANCE_MAX]; static int sudo_debug_num_instances; static char sudo_debug_pidstr[(((sizeof(int) * 8) + 2) / 3) + 3]; static size_t sudo_debug_pidlen; static fd_set sudo_debug_fdset; /* XXX - make dynamic */ static int sudo_debug_max_fd = -1; /* Default instance index to use for common utility functions. */ static int sudo_debug_default_instance = -1; /* * Create a new output file for the specified debug instance. */ static struct sudo_debug_output * sudo_debug_new_output(struct sudo_debug_instance *instance, struct sudo_debug_file *debug_file) { char *buf, *cp, *subsys, *pri; struct sudo_debug_output *output; int i, j; /* Create new output for the instance. */ /* XXX - reuse fd for existing filename? */ output = sudo_emalloc(sizeof(*output)); output->settings = sudo_emallocarray(instance->num_subsystems, sizeof(int)); output->filename = sudo_estrdup(debug_file->debug_file); output->fd = -1; /* Init per-subsystems settings to -1 since 0 is a valid priority. */ for (i = 0; i < instance->num_subsystems; i++) output->settings[i] = -1; /* Open debug file. */ output->fd = open(output->filename, O_WRONLY|O_APPEND, S_IRUSR|S_IWUSR); if (output->fd == -1) { /* Create debug file as needed and set group ownership. */ if (errno == ENOENT) { output->fd = open(output->filename, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR|S_IWUSR); } if (output->fd == -1) return NULL; ignore_result(fchown(output->fd, (uid_t)-1, 0)); } (void)fcntl(output->fd, F_SETFD, FD_CLOEXEC); /* XXX - realloc sudo_debug_fdset as needed (or use other bitmap). */ if (output->fd < FD_SETSIZE) { FD_SET(output->fd, &sudo_debug_fdset); if (output->fd > sudo_debug_max_fd) sudo_debug_max_fd = output->fd; } /* Parse Debug conf string. */ if ((buf = strdup(debug_file->debug_flags)) == NULL) { /* XXX - free output on error or make non-destructive */ return NULL; } for ((cp = strtok(buf, ",")); cp != NULL; (cp = strtok(NULL, ","))) { /* Should be in the form subsys@pri. */ subsys = cp; if ((pri = strchr(cp, '@')) == NULL) continue; *pri++ = '\0'; /* Look up priority and subsystem, fill in sudo_debug_settings[]. */ for (i = 0; sudo_debug_priorities[i] != NULL; i++) { if (strcasecmp(pri, sudo_debug_priorities[i]) == 0) { for (j = 0; instance->subsystems[j] != NULL; j++) { if (strcasecmp(subsys, "all") == 0) { output->settings[j] = i; continue; } if (strcasecmp(subsys, instance->subsystems[j]) == 0) { output->settings[j] = i; break; } } break; } } } free(buf); return output; } /* * Register a program/plugin with the debug framework, * parses settings string from sudo.conf and opens debugfile. * If subsystem names are specified they override the default values. * NOTE: subsystems must not be freed by caller unless deregistered. * Returns instance index on success or -1 if cannot open debugfile. */ int sudo_debug_register(const char *program, const char *const subsystems[], int num_subsystems, struct sudo_conf_debug_file_list *debug_files) { struct sudo_debug_instance *instance = NULL; struct sudo_debug_output *output; struct sudo_debug_file *debug_file; int idx, free_idx = -1; if (debug_files == NULL || num_subsystems < 0) return -1; /* Use default subsystem names if none are provided. */ if (subsystems == NULL || num_subsystems == 0) { subsystems = sudo_debug_default_subsystems; num_subsystems = NUM_SUBSYSTEMS; } /* Search for existing instance. */ for (idx = 0; idx < sudo_debug_num_instances; idx++) { if (sudo_debug_instances[idx] == NULL) { free_idx = idx; continue; } if (sudo_debug_instances[idx]->subsystems == subsystems && strcmp(sudo_debug_instances[idx]->program, program) == 0) { instance = sudo_debug_instances[idx]; break; } } if (instance == NULL) { if (free_idx != -1) idx = free_idx; if (idx == SUDO_DEBUG_INSTANCE_MAX) { /* XXX - realloc? */ sudo_warnx_nodebug("too many debug instances (max %d)", SUDO_DEBUG_INSTANCE_MAX); return -1; } if (idx != sudo_debug_num_instances && idx != free_idx) { sudo_warnx_nodebug("%s: instance number mismatch: expected %d, got %u", __func__, idx, sudo_debug_num_instances); return -1; } instance = sudo_emalloc(sizeof(*instance)); instance->program = sudo_estrdup(program); instance->subsystems = subsystems; instance->num_subsystems = num_subsystems; SLIST_INIT(&instance->outputs); sudo_debug_instances[idx] = instance; if (idx == sudo_debug_num_instances) sudo_debug_num_instances++; } TAILQ_FOREACH(debug_file, debug_files, entries) { output = sudo_debug_new_output(instance, debug_file); if (output != NULL) SLIST_INSERT_HEAD(&instance->outputs, output, entries); } /* Stash the pid string so we only have to format it once. */ if (sudo_debug_pidlen == 0) { (void)snprintf(sudo_debug_pidstr, sizeof(sudo_debug_pidstr), "[%d] ", (int)getpid()); sudo_debug_pidlen = strlen(sudo_debug_pidstr); } /* Convert index to instance. */ return SUDO_DEBUG_MKINSTANCE(idx); } /* * De-register the specified instance from the debug subsystem * and free up any associated data structures. */ int sudo_debug_deregister(int instance_id) { struct sudo_debug_instance *instance; struct sudo_debug_output *output, *next; int idx; idx = SUDO_DEBUG_INSTANCE(instance_id); if (idx < 0 || idx >= sudo_debug_num_instances) { sudo_warnx_nodebug("%s: invalid instance number %d, max %u", __func__, idx + 1, sudo_debug_num_instances); return -1; } /* Reset default instance as needed. */ if (sudo_debug_default_instance == idx) sudo_debug_default_instance = -1; instance = sudo_debug_instances[idx]; if (instance == NULL) return -1; /* already deregistered */ /* Free up instance data, note that subsystems[] is owned by caller. */ sudo_debug_instances[idx] = NULL; SLIST_FOREACH_SAFE(output, &instance->outputs, entries, next) { close(output->fd); sudo_efree(output->filename); sudo_efree(output->settings); sudo_efree(output); } sudo_efree(instance->program); sudo_efree(instance); if (idx == sudo_debug_num_instances - 1) sudo_debug_num_instances--; return 0; } int sudo_debug_get_instance(const char *program) { size_t proglen; int idx; /* Treat "sudoedit" as an alias for "sudo". */ proglen = strlen(program); if (proglen > 4 && strcmp(program + proglen - 4, "edit") == 0) proglen -= 4; for (idx = 0; idx < sudo_debug_num_instances; idx++) { if (sudo_debug_instances[idx] == NULL) continue; if (strncmp(sudo_debug_instances[idx]->program, program, proglen) == 0 && sudo_debug_instances[idx]->program[proglen] == '\0') return SUDO_DEBUG_MKINSTANCE(idx); } return SUDO_DEBUG_INSTANCE_INITIALIZER; } int sudo_debug_set_output_fd(int level, int ofd, int nfd) { struct sudo_debug_instance *instance; struct sudo_debug_output *output; int idx; idx = SUDO_DEBUG_INSTANCE(level); if (idx < 0 || idx >= sudo_debug_num_instances) { sudo_warnx_nodebug("%s: invalid instance number %d, max %u", __func__, idx + 1, sudo_debug_num_instances); return -1; } instance = sudo_debug_instances[idx]; SLIST_FOREACH(output, &instance->outputs, entries) { if (output->fd == ofd) output->fd = nfd; } return 0; } pid_t sudo_debug_fork(void) { pid_t pid; if ((pid = fork()) == 0) { (void)snprintf(sudo_debug_pidstr, sizeof(sudo_debug_pidstr), "[%d] ", (int)getpid()); sudo_debug_pidlen = strlen(sudo_debug_pidstr); } return pid; } void sudo_debug_enter(const char *func, const char *file, int line, int subsys) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "-> %s @ %s:%d", func, file, line); } void sudo_debug_exit(const char *func, const char *file, int line, int subsys) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d", func, file, line); } void sudo_debug_exit_int(const char *func, const char *file, int line, int subsys, int rval) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %d", func, file, line, rval); } void sudo_debug_exit_long(const char *func, const char *file, int line, int subsys, long rval) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %ld", func, file, line, rval); } void sudo_debug_exit_size_t(const char *func, const char *file, int line, int subsys, size_t rval) { /* XXX - should use %zu but our snprintf.c doesn't support it */ sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %lu", func, file, line, (unsigned long)rval); } /* We use int, not bool, here for functions that return -1 on error. */ void sudo_debug_exit_bool(const char *func, const char *file, int line, int subsys, int rval) { if (rval == true || rval == false) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %s", func, file, line, rval ? "true" : "false"); } else { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %d", func, file, line, rval); } } void sudo_debug_exit_str(const char *func, const char *file, int line, int subsys, const char *rval) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %s", func, file, line, rval ? rval : "(null)"); } void sudo_debug_exit_str_masked(const char *func, const char *file, int line, int subsys, const char *rval) { static const char stars[] = "********************************************************************************"; int len = rval ? strlen(rval) : sizeof("(null)") - 1; sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %.*s", func, file, line, len, rval ? stars : "(null)"); } void sudo_debug_exit_ptr(const char *func, const char *file, int line, int subsys, const void *rval) { sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE, "<- %s @ %s:%d := %p", func, file, line, rval); } void sudo_debug_write2(int fd, const char *func, const char *file, int lineno, const char *str, int len, int errnum) { char *timestr, numbuf[(((sizeof(int) * 8) + 2) / 3) + 2]; time_t now; struct iovec iov[12]; int iovcnt = 3; /* Prepend program name and pid with a trailing space. */ iov[1].iov_base = (char *)getprogname(); iov[1].iov_len = strlen(iov[1].iov_base); iov[2].iov_base = sudo_debug_pidstr; iov[2].iov_len = sudo_debug_pidlen; /* Add string, trimming any trailing newlines. */ while (len > 0 && str[len - 1] == '\n') len--; if (len > 0) { iov[iovcnt].iov_base = (char *)str; iov[iovcnt].iov_len = len; iovcnt++; } /* Append error string if errno is specified. */ if (errnum) { if (len > 0) { iov[iovcnt].iov_base = ": "; iov[iovcnt].iov_len = 2; iovcnt++; } iov[iovcnt].iov_base = strerror(errnum); iov[iovcnt].iov_len = strlen(iov[iovcnt].iov_base); iovcnt++; } /* If function, file and lineno are specified, append them. */ if (func != NULL && file != NULL && lineno != 0) { iov[iovcnt].iov_base = " @ "; iov[iovcnt].iov_len = 3; iovcnt++; iov[iovcnt].iov_base = (char *)func; iov[iovcnt].iov_len = strlen(func); iovcnt++; iov[iovcnt].iov_base = "() "; iov[iovcnt].iov_len = 3; iovcnt++; iov[iovcnt].iov_base = (char *)file; iov[iovcnt].iov_len = strlen(file); iovcnt++; (void)snprintf(numbuf, sizeof(numbuf), ":%d", lineno); iov[iovcnt].iov_base = numbuf; iov[iovcnt].iov_len = strlen(numbuf); iovcnt++; } /* Append newline. */ iov[iovcnt].iov_base = "\n"; iov[iovcnt].iov_len = 1; iovcnt++; /* Do timestamp last due to ctime's static buffer. */ time(&now); timestr = ctime(&now) + 4; timestr[15] = ' '; /* replace year with a space */ timestr[16] = '\0'; iov[0].iov_base = timestr; iov[0].iov_len = 16; /* Write message in a single syscall */ ignore_result(writev(fd, iov, iovcnt)); } void sudo_debug_vprintf2(const char *func, const char *file, int lineno, int level, const char *fmt, va_list ap) { int buflen, idx, pri, subsys, saved_errno = errno; char static_buf[1024], *buf = static_buf; struct sudo_debug_instance *instance; struct sudo_debug_output *output; va_list ap2; if (sudo_debug_num_instances == 0) goto out; /* Extract instance index, priority and subsystem from level. */ idx = SUDO_DEBUG_INSTANCE(level); pri = SUDO_DEBUG_PRI(level); subsys = SUDO_DEBUG_SUBSYS(level); /* Find matching instance. */ if (idx < 0) { /* Check for default instance, else we are not initialized. */ if (sudo_debug_default_instance < 0 || SUDO_DEBUG_MKINSTANCE(idx) != SUDO_DEBUG_INSTANCE_DEFAULT) { goto out; } idx = sudo_debug_default_instance; } else if (idx >= sudo_debug_num_instances) { sudo_warnx_nodebug("%s: invalid instance number %d, max %u", __func__, idx + 1, sudo_debug_num_instances); goto out; } instance = sudo_debug_instances[idx]; if (instance == NULL) { sudo_warnx_nodebug("%s: unregistered instance index %d", __func__, idx); goto out; } SLIST_FOREACH(output, &instance->outputs, entries) { /* Make sure we want debug info at this level. */ if (subsys < instance->num_subsystems && output->settings[subsys] >= pri) { va_copy(ap2, ap); buflen = fmt ? vsnprintf(static_buf, sizeof(static_buf), fmt, ap) : 0; if (buflen >= (int)sizeof(static_buf)) { /* Not enough room in static buf, allocate dynamically. */ buflen = vasprintf(&buf, fmt, ap2); } if (buflen != -1) { int errcode = ISSET(level, SUDO_DEBUG_ERRNO) ? saved_errno : 0; if (ISSET(level, SUDO_DEBUG_LINENO)) sudo_debug_write2(output->fd, func, file, lineno, buf, buflen, errcode); else sudo_debug_write2(output->fd, NULL, NULL, 0, buf, buflen, errcode); if (buf != static_buf) { sudo_efree(buf); buf = static_buf; } } va_end(ap2); } } out: errno = saved_errno; } #ifdef NO_VARIADIC_MACROS void sudo_debug_printf_nvm(int pri, const char *fmt, ...) { va_list ap; va_start(ap, fmt); sudo_debug_vprintf2(NULL, NULL, 0, pri, fmt, ap); va_end(ap); } #endif /* NO_VARIADIC_MACROS */ void sudo_debug_printf2(const char *func, const char *file, int lineno, int level, const char *fmt, ...) { va_list ap; va_start(ap, fmt); sudo_debug_vprintf2(func, file, lineno, level, fmt, ap); va_end(ap); } #define EXEC_PREFIX "exec " void sudo_debug_execve2(int level, const char *path, char *const argv[], char *const envp[]) { int buflen, idx, pri, subsys, saved_errno = errno; struct sudo_debug_instance *instance; struct sudo_debug_output *output; char * const *av; char *buf, *cp; size_t plen; if (sudo_debug_num_instances == 0) goto out; /* Extract instance index, priority and subsystem from level. */ idx = SUDO_DEBUG_INSTANCE(level); pri = SUDO_DEBUG_PRI(level); subsys = SUDO_DEBUG_SUBSYS(level); /* Find matching instance. */ if (idx < 0) { /* Check for default instance, else we are not initialized. */ if (sudo_debug_default_instance < 0 || SUDO_DEBUG_MKINSTANCE(idx) != SUDO_DEBUG_INSTANCE_DEFAULT) { goto out; } idx = sudo_debug_default_instance; } else if (idx >= sudo_debug_num_instances) { sudo_warnx_nodebug("%s: invalid instance number %d, max %u", __func__, idx + 1, sudo_debug_num_instances); goto out; } instance = sudo_debug_instances[idx]; if (instance == NULL) { sudo_warnx_nodebug("%s: unregistered instance index %d", __func__, idx); goto out; } if (subsys >= instance->num_subsystems) goto out; /* XXX - use static buffer if possible */ SLIST_FOREACH(output, &instance->outputs, entries) { bool log_envp = false; /* Make sure we want debug info at this level. */ if (output->settings[subsys] < pri) continue; /* Log envp for debug level "debug". */ if (output->settings[subsys] >= SUDO_DEBUG_DEBUG - 1 && envp[0] != NULL) log_envp = true; /* Alloc and build up buffer. */ plen = strlen(path); buflen = sizeof(EXEC_PREFIX) -1 + plen; if (argv[0] != NULL) { buflen += sizeof(" []") - 1; for (av = argv; *av; av++) buflen += strlen(*av) + 1; buflen--; } if (log_envp) { buflen += sizeof(" []") - 1; for (av = envp; *av; av++) buflen += strlen(*av) + 1; buflen--; } buf = malloc(buflen + 1); if (buf == NULL) goto out; /* Copy prefix and command. */ memcpy(buf, EXEC_PREFIX, sizeof(EXEC_PREFIX) - 1); cp = buf + sizeof(EXEC_PREFIX) - 1; memcpy(cp, path, plen); cp += plen; /* Copy argv. */ if (argv[0] != NULL) { *cp++ = ' '; *cp++ = '['; for (av = argv; *av; av++) { size_t avlen = strlen(*av); memcpy(cp, *av, avlen); cp += avlen; *cp++ = ' '; } cp[-1] = ']'; } if (log_envp) { *cp++ = ' '; *cp++ = '['; for (av = envp; *av; av++) { size_t avlen = strlen(*av); memcpy(cp, *av, avlen); cp += avlen; *cp++ = ' '; } cp[-1] = ']'; } *cp = '\0'; sudo_debug_write(output->fd, buf, buflen, 0); free(buf); } out: errno = saved_errno; } /* * Returns the default instance or SUDO_DEBUG_INSTANCE_INITIALIZER * if no default instance is set. */ int sudo_debug_get_default_instance(void) { return SUDO_DEBUG_MKINSTANCE(sudo_debug_default_instance); } /* * Sets a new default instance, returning the old one. * Note that the old instance may be SUDO_DEBUG_INSTANCE_INITIALIZER * of this is the only instance. */ int sudo_debug_set_default_instance(int inst) { const int idx = SUDO_DEBUG_INSTANCE(inst); const int old_idx = sudo_debug_default_instance; if (idx >= -1 && idx < sudo_debug_num_instances) sudo_debug_default_instance = idx; return SUDO_DEBUG_MKINSTANCE(old_idx); } /* * Replace the ofd with nfd in all outputs if present. * Also updates sudo_debug_fdset. */ void sudo_debug_update_fd(int ofd, int nfd) { int idx; if (ofd <= sudo_debug_max_fd && FD_ISSET(ofd, &sudo_debug_fdset)) { /* Update sudo_debug_fdset. */ FD_CLR(ofd, &sudo_debug_fdset); FD_SET(nfd, &sudo_debug_fdset); /* Update the outputs. */ for (idx = 0; idx < sudo_debug_num_instances; idx++) { struct sudo_debug_instance *instance; struct sudo_debug_output *output; instance = sudo_debug_instances[idx]; if (instance == NULL) continue; SLIST_FOREACH(output, &instance->outputs, entries) { if (output->fd == ofd) output->fd = nfd; } } } } /* * Returns the highest debug output fd or -1 if no debug files open. * Fills in fdsetp with the value of sudo_debug_fdset. */ int sudo_debug_get_fds(fd_set **fdsetp) { *fdsetp = &sudo_debug_fdset; return sudo_debug_max_fd; }