/* * Copyright (c) 1996, 1998-2005, 2007-2015 * 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. * * Sponsored in part by the Defense Advanced Research Projects * Agency (DARPA) and Air Force Research Laboratory, Air Force * Materiel Command, USAF, under agreement number F39502-99-1-0512. */ /* * Lock the sudoers file for safe editing (ala vipw) and check for parse errors. */ #ifdef __TANDEM # include #endif #include #include #include #include #include #include #ifndef __TANDEM # include #endif #include #include #ifdef STDC_HEADERS # include # include #else # ifdef HAVE_STDLIB_H # include # endif #endif /* STDC_HEADERS */ #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 #include #include #include #include #include #ifdef TIME_WITH_SYS_TIME # include #endif #ifndef HAVE_STRUCT_TIMESPEC # include "compat/timespec.h" #endif #include "sudoers.h" #include "parse.h" #include "redblack.h" #include "sudoers_version.h" #include "sudo_conf.h" #include #ifdef HAVE_GETOPT_LONG # include # else # include "compat/getopt.h" #endif /* HAVE_GETOPT_LONG */ struct sudoersfile { TAILQ_ENTRY(sudoersfile) entries; char *path; char *tpath; int fd; int modified; int doedit; }; TAILQ_HEAD(sudoersfile_list, sudoersfile); /* * Function prototypes */ static void quit(int); static void get_hostname(void); static int whatnow(void); static int check_aliases(bool, bool); static char *get_editor(int *editor_argc, char ***editor_argv); static bool check_syntax(const char *, bool, bool, bool); static bool edit_sudoers(struct sudoersfile *, char *, int, char **, int); static bool install_sudoers(struct sudoersfile *, bool); static int print_unused(void *, void *); static bool reparse_sudoers(char *, int, char **, bool, bool); static int run_command(char *, char **); static void parse_sudoers_options(void); static void setup_signals(void); static void help(void) __attribute__((__noreturn__)); static void usage(int); static void visudo_cleanup(void); extern bool export_sudoers(const char *, const char *, bool, bool); extern void sudoerserror(const char *); extern void sudoersrestart(FILE *); /* * Globals */ struct sudo_user sudo_user; struct passwd *list_pw; static struct sudoersfile_list sudoerslist = TAILQ_HEAD_INITIALIZER(sudoerslist); static struct rbtree *alias_freelist; static bool checkonly; static const char short_opts[] = "cf:hqsVx:"; static struct option long_opts[] = { { "check", no_argument, NULL, 'c' }, { "export", required_argument, NULL, 'x' }, { "file", required_argument, NULL, 'f' }, { "help", no_argument, NULL, 'h' }, { "quiet", no_argument, NULL, 'q' }, { "strict", no_argument, NULL, 's' }, { "version", no_argument, NULL, 'V' }, { NULL, no_argument, NULL, '\0' }, }; __dso_public int main(int argc, char *argv[]); int main(int argc, char *argv[]) { struct sudoersfile *sp; char *editor, **editor_argv; int ch, editor_argc, exitcode = 0; bool quiet, strict, oldperms; const char *export_path; debug_decl(main, SUDOERS_DEBUG_MAIN) #if defined(SUDO_DEVEL) && defined(__OpenBSD__) { extern char *malloc_options; malloc_options = "AFGJPR"; } #endif initprogname(argc > 0 ? argv[0] : "visudo"); if (!sudoers_initlocale(setlocale(LC_ALL, ""), def_sudoers_locale)) sudo_fatalx(U_("unable to allocate memory")); bindtextdomain("sudoers", LOCALEDIR); /* XXX - should have visudo domain */ textdomain("sudoers"); if (argc < 1) usage(1); /* Register fatal/fatalx callback. */ sudo_fatal_callback_register(visudo_cleanup); /* Read debug and plugin sections of sudo.conf. */ sudo_conf_read(NULL, SUDO_CONF_DEBUG|SUDO_CONF_PLUGINS); /* Initialize the debug subsystem. */ sudoers_debug_register(getprogname(), sudo_conf_debug_files(getprogname())); /* Parse sudoers plugin options, if any. */ parse_sudoers_options(); /* * Arg handling. */ checkonly = oldperms = quiet = strict = false; export_path = NULL; while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (ch) { case 'V': (void) printf(_("%s version %s\n"), getprogname(), PACKAGE_VERSION); (void) printf(_("%s grammar version %d\n"), getprogname(), SUDOERS_GRAMMAR_VERSION); goto done; case 'c': checkonly = true; /* check mode */ break; case 'f': sudoers_file = optarg; /* sudoers file path */ oldperms = true; break; case 'h': help(); break; case 's': strict = true; /* strict mode */ break; case 'q': quiet = true; /* quiet mode */ break; case 'x': export_path = optarg; /* export mode */ break; default: usage(1); } } /* There should be no other command line arguments. */ if (argc - optind != 0) usage(1); if (sudo_setpwent() == -1 || sudo_setgrent() == -1) sudo_fatalx(U_("unable to allocate memory")); /* Mock up a fake sudo_user struct. */ user_cmnd = user_base = ""; if ((sudo_user.pw = sudo_getpwuid(getuid())) == NULL) sudo_fatalx(U_("you do not exist in the %s database"), "passwd"); get_hostname(); /* Setup defaults data structures. */ if (!init_defaults()) sudo_fatalx(U_("unable to initialize sudoers default values")); if (checkonly) { exitcode = check_syntax(sudoers_file, quiet, strict, oldperms) ? 0 : 1; goto done; } if (export_path != NULL) { exitcode = export_sudoers(sudoers_file, export_path, quiet, strict) ? 0 : 1; goto done; } /* * Parse the existing sudoers file(s) to highlight any existing * errors and to pull in editor and env_editor conf values. */ if ((sudoersin = open_sudoers(sudoers_file, true, NULL)) == NULL) exit(1); init_parser(sudoers_file, false); sudoersparse(); (void) update_defaults(SETDEF_GENERIC|SETDEF_HOST|SETDEF_USER); editor = get_editor(&editor_argc, &editor_argv); /* Install signal handlers to clean up temp files if we are killed. */ setup_signals(); /* Edit the sudoers file(s) */ TAILQ_FOREACH(sp, &sudoerslist, entries) { if (!sp->doedit) continue; if (sp != TAILQ_FIRST(&sudoerslist)) { printf(_("press return to edit %s: "), sp->path); while ((ch = getchar()) != EOF && ch != '\n') continue; } edit_sudoers(sp, editor, editor_argc, editor_argv, -1); } /* * Check edited files for a parse error, re-edit any that fail * and install the edited files as needed. */ if (reparse_sudoers(editor, editor_argc, editor_argv, strict, quiet)) { TAILQ_FOREACH(sp, &sudoerslist, entries) { if (sp->doedit) (void) install_sudoers(sp, oldperms); } } done: sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode); exit(exitcode); } static char * get_editor(int *editor_argc, char ***editor_argv) { char *editor, *editor_path = NULL, **whitelist = NULL; static char *files[] = { "+1", "sudoers" }; unsigned int whitelist_len = 0; debug_decl(get_editor, SUDOERS_DEBUG_UTIL) /* Build up editor whitelist from def_editor unless env_editor is set. */ if (!def_env_editor) { const char *cp, *ep; const char *def_editor_end = def_editor + strlen(def_editor); /* Count number of entries in whitelist and split into a list. */ for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep); cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) { whitelist_len++; } whitelist = reallocarray(NULL, whitelist_len + 1, sizeof(char *)); if (whitelist == NULL) sudo_fatalx(U_("unable to allocate memory")); whitelist_len = 0; for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep); cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) { whitelist[whitelist_len] = strndup(cp, (size_t)(ep - cp)); if (whitelist[whitelist_len] == NULL) sudo_fatalx(U_("unable to allocate memory")); whitelist_len++; } whitelist[whitelist_len] = NULL; } /* First try to use user's VISUAL or EDITOR environment vars. */ if ((editor = getenv("VISUAL")) == NULL || *editor == '\0') editor = getenv("EDITOR"); if (editor && *editor == '\0') editor = NULL; if (editor != NULL) { editor_path = resolve_editor(editor, strlen(editor), 2, files, editor_argc, editor_argv, whitelist); if (def_env_editor && editor_path == NULL) { /* If we are honoring $EDITOR this is a fatal error. */ sudo_fatalx(U_("specified editor (%s) doesn't exist"), editor); } } if (editor_path == NULL) { /* def_editor could be a path, split it up, avoiding strtok() */ const char *def_editor_end = def_editor + strlen(def_editor); const char *cp, *ep; for (cp = sudo_strsplit(def_editor, def_editor_end, ":", &ep); cp != NULL; cp = sudo_strsplit(NULL, def_editor_end, ":", &ep)) { editor_path = resolve_editor(cp, (size_t)(ep - cp), 2, files, editor_argc, editor_argv, whitelist); if (editor_path == NULL && errno != ENOENT) debug_return_str(NULL); } while (ep != NULL && editor_path == NULL); } if (editor_path == NULL) sudo_fatalx(U_("no editor found (editor path = %s)"), def_editor); if (whitelist != NULL) { while (whitelist_len--) free(whitelist[whitelist_len]); free(whitelist); } debug_return_str(editor_path); } /* * List of editors that support the "+lineno" command line syntax. * If an entry starts with '*' the tail end of the string is matched. * No other wild cards are supported. */ static char *lineno_editors[] = { "ex", "nex", "vi", "nvi", "vim", "elvis", "*macs", "mg", "vile", "jove", "pico", "nano", "ee", "joe", "zile", NULL }; /* * Check whether or not the specified editor matched lineno_editors[]. * Returns true if yes, false if no. */ static bool editor_supports_plus(const char *editor) { const char *editor_base = strrchr(editor, '/'); const char *cp; char **av; debug_decl(editor_supports_plus, SUDOERS_DEBUG_UTIL) if (editor_base != NULL) editor_base++; else editor_base = editor; if (*editor_base == 'r') editor_base++; for (av = lineno_editors; (cp = *av) != NULL; av++) { /* We only handle a leading '*' wildcard. */ if (*cp == '*') { size_t blen = strlen(editor_base); size_t clen = strlen(++cp); if (blen >= clen) { if (strcmp(cp, editor_base + blen - clen) == 0) break; } } else if (strcmp(cp, editor_base) == 0) break; } debug_return_bool(cp != NULL); } /* * Edit each sudoers file. * Returns true on success, else false. */ static bool edit_sudoers(struct sudoersfile *sp, char *editor, int editor_argc, char **editor_argv, int lineno) { int tfd; /* sudoers temp file descriptor */ bool modified; /* was the file modified? */ int ac; /* argument count */ char buf[4096]; /* buffer used for copying files */ char linestr[64]; /* string version of lineno */ struct timespec ts, times[2]; /* time before and after edit */ struct timespec orig_mtim; /* starting mtime of sudoers file */ off_t orig_size; /* starting size of sudoers file */ ssize_t nread; /* number of bytes read */ struct stat sb; /* stat buffer */ bool rval = false; /* return value */ debug_decl(edit_sudoers, SUDOERS_DEBUG_UTIL) if (fstat(sp->fd, &sb) == -1) sudo_fatal(U_("unable to stat %s"), sp->path); orig_size = sb.st_size; mtim_get(&sb, orig_mtim); /* Create the temp file if needed and set timestamp. */ if (sp->tpath == NULL) { if (asprintf(&sp->tpath, "%s.tmp", sp->path) == -1) sudo_fatalx(U_("unable to allocate memory")); tfd = open(sp->tpath, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (tfd < 0) sudo_fatal("%s", sp->tpath); /* Copy sp->path -> sp->tpath and reset the mtime. */ if (orig_size != 0) { (void) lseek(sp->fd, (off_t)0, SEEK_SET); while ((nread = read(sp->fd, buf, sizeof(buf))) > 0) if (write(tfd, buf, nread) != nread) sudo_fatal(U_("write error")); /* Add missing newline at EOF if needed. */ if (nread > 0 && buf[nread - 1] != '\n') { buf[0] = '\n'; if (write(tfd, buf, 1) != 1) sudo_fatal(U_("write error")); } } (void) close(tfd); } times[0].tv_sec = times[1].tv_sec = orig_mtim.tv_sec; times[0].tv_nsec = times[1].tv_nsec = orig_mtim.tv_nsec; (void) utimensat(AT_FDCWD, sp->tpath, times, 0); /* Disable +lineno if editor doesn't support it. */ if (lineno > 0 && !editor_supports_plus(editor)) lineno = -1; /* * We pre-allocated 2 extra spaces for "+n filename" in argv. * Replace those placeholders with the real values. */ ac = editor_argc - 2; if (lineno > 0) { (void)snprintf(linestr, sizeof(linestr), "+%d", lineno); editor_argv[ac++] = linestr; } editor_argv[ac++] = sp->tpath; editor_argv[ac++] = NULL; /* * Do the edit: * We cannot check the editor's exit value against 0 since * XPG4 specifies that vi's exit value is a function of the * number of errors during editing (?!?!). */ if (sudo_gettime_real(×[0]) == -1) { sudo_warn(U_("unable to read the clock")); goto done; } if (run_command(editor, editor_argv) != -1) { if (sudo_gettime_real(×[1]) == -1) { sudo_warn(U_("unable to read the clock")); goto done; } /* * Sanity checks. */ if (stat(sp->tpath, &sb) < 0) { sudo_warnx(U_("unable to stat temporary file (%s), %s unchanged"), sp->tpath, sp->path); goto done; } if (sb.st_size == 0 && orig_size != 0) { /* Avoid accidental zeroing of main sudoers file. */ if (sp == TAILQ_FIRST(&sudoerslist)) { sudo_warnx(U_("zero length temporary file (%s), %s unchanged"), sp->tpath, sp->path); goto done; } } } else { sudo_warnx(U_("editor (%s) failed, %s unchanged"), editor, sp->path); goto done; } /* Set modified bit if the user changed the file. */ modified = true; mtim_get(&sb, ts); if (orig_size == sb.st_size && sudo_timespeccmp(&orig_mtim, &ts, ==)) { /* * If mtime and size match but the user spent no measurable * time in the editor we can't tell if the file was changed. */ if (sudo_timespeccmp(×[0], ×[1], !=)) modified = false; } /* * If modified in this edit session, mark as modified. */ if (modified) sp->modified = modified; else sudo_warnx(U_("%s unchanged"), sp->tpath); rval = true; done: debug_return_bool(rval); } /* * Parse sudoers after editing and re-edit any ones that caused a parse error. */ static bool reparse_sudoers(char *editor, int editor_argc, char **editor_argv, bool strict, bool quiet) { struct sudoersfile *sp, *last; FILE *fp; int ch; debug_decl(reparse_sudoers, SUDOERS_DEBUG_UTIL) /* * Parse the edited sudoers files and do sanity checking */ while ((sp = TAILQ_FIRST(&sudoerslist)) != NULL) { last = TAILQ_LAST(&sudoerslist, sudoersfile_list); fp = fopen(sp->tpath, "r+"); if (fp == NULL) sudo_fatalx(U_("unable to re-open temporary file (%s), %s unchanged."), sp->tpath, sp->path); /* Clean slate for each parse */ if (!init_defaults()) sudo_fatalx(U_("unable to initialize sudoers default values")); init_parser(sp->path, quiet); /* Parse the sudoers temp file(s) */ sudoersrestart(fp); if (sudoersparse() && !parse_error) { sudo_warnx(U_("unabled to parse temporary file (%s), unknown error"), sp->tpath); parse_error = true; errorfile = sp->path; } fclose(sudoersin); if (!parse_error) { if (!check_defaults(SETDEF_ALL, quiet) || check_aliases(strict, quiet) != 0) { parse_error = true; errorfile = NULL; } } /* * Got an error, prompt the user for what to do now. */ if (parse_error) { switch (whatnow()) { case 'Q': parse_error = false; /* ignore parse error */ break; case 'x': visudo_cleanup(); /* discard changes */ debug_return_bool(false); case 'e': default: /* Edit file with the parse error */ TAILQ_FOREACH(sp, &sudoerslist, entries) { if (errorfile == NULL || strcmp(sp->path, errorfile) == 0) { edit_sudoers(sp, editor, editor_argc, editor_argv, errorlineno); if (errorfile != NULL) break; } } if (errorfile != NULL && sp == NULL) { sudo_fatalx(U_("internal error, unable to find %s in list!"), sudoers); } break; } } /* If any new #include directives were added, edit them too. */ for (sp = TAILQ_NEXT(last, entries); sp != NULL; sp = TAILQ_NEXT(sp, entries)) { printf(_("press return to edit %s: "), sp->path); while ((ch = getchar()) != EOF && ch != '\n') continue; edit_sudoers(sp, editor, editor_argc, editor_argv, errorlineno); } /* If all sudoers files parsed OK we are done. */ if (!parse_error) break; } debug_return_bool(true); } /* * Set the owner and mode on a sudoers temp file and * move it into place. Returns true on success, else false. */ static bool install_sudoers(struct sudoersfile *sp, bool oldperms) { struct stat sb; bool rval = false; debug_decl(install_sudoers, SUDOERS_DEBUG_UTIL) if (!sp->modified) { /* * No changes but fix owner/mode if needed. */ (void) unlink(sp->tpath); if (!oldperms && fstat(sp->fd, &sb) != -1) { if (sb.st_uid != sudoers_uid || sb.st_gid != sudoers_gid) ignore_result(chown(sp->path, sudoers_uid, sudoers_gid)); if ((sb.st_mode & 0777) != sudoers_mode) ignore_result(chmod(sp->path, sudoers_mode)); } rval = true; goto done; } /* * Change mode and ownership of temp file so when * we move it to sp->path things are kosher. */ if (oldperms) { /* Use perms of the existing file. */ if (fstat(sp->fd, &sb) == -1) sudo_fatal(U_("unable to stat %s"), sp->path); if (chown(sp->tpath, sb.st_uid, sb.st_gid) != 0) { sudo_warn(U_("unable to set (uid, gid) of %s to (%u, %u)"), sp->tpath, (unsigned int)sb.st_uid, (unsigned int)sb.st_gid); } if (chmod(sp->tpath, sb.st_mode & 0777) != 0) { sudo_warn(U_("unable to change mode of %s to 0%o"), sp->tpath, (unsigned int)(sb.st_mode & 0777)); } } else { if (chown(sp->tpath, sudoers_uid, sudoers_gid) != 0) { sudo_warn(U_("unable to set (uid, gid) of %s to (%u, %u)"), sp->tpath, sudoers_uid, sudoers_gid); goto done; } if (chmod(sp->tpath, sudoers_mode) != 0) { sudo_warn(U_("unable to change mode of %s to 0%o"), sp->tpath, (unsigned int)sudoers_mode); goto done; } } /* * Now that sp->tpath is sane (parses ok) it needs to be * rename(2)'d to sp->path. If the rename(2) fails we try using * mv(1) in case sp->tpath and sp->path are on different file systems. */ if (rename(sp->tpath, sp->path) == 0) { free(sp->tpath); sp->tpath = NULL; } else { if (errno == EXDEV) { char *av[4]; sudo_warnx(U_("%s and %s not on the same file system, using mv to rename"), sp->tpath, sp->path); /* Build up argument vector for the command */ if ((av[0] = strrchr(_PATH_MV, '/')) != NULL) av[0]++; else av[0] = _PATH_MV; av[1] = sp->tpath; av[2] = sp->path; av[3] = NULL; /* And run it... */ if (run_command(_PATH_MV, av)) { sudo_warnx(U_("command failed: '%s %s %s', %s unchanged"), _PATH_MV, sp->tpath, sp->path, sp->path); (void) unlink(sp->tpath); free(sp->tpath); sp->tpath = NULL; goto done; } free(sp->tpath); sp->tpath = NULL; } else { sudo_warn(U_("error renaming %s, %s unchanged"), sp->tpath, sp->path); (void) unlink(sp->tpath); goto done; } } rval = true; done: debug_return_bool(rval); } /* STUB */ bool init_envtables(void) { return true; } /* STUB */ bool user_is_exempt(void) { return false; } /* STUB */ void sudo_setspent(void) { return; } /* STUB */ void sudo_endspent(void) { return; } /* STUB */ int group_plugin_query(const char *user, const char *group, const struct passwd *pw) { return false; } /* STUB */ struct interface *get_interfaces(void) { return NULL; } /* * Assuming a parse error occurred, prompt the user for what they want * to do now. Returns the first letter of their choice. */ static int whatnow(void) { int choice, c; debug_decl(whatnow, SUDOERS_DEBUG_UTIL) for (;;) { (void) fputs(_("What now? "), stdout); choice = getchar(); for (c = choice; c != '\n' && c != EOF;) c = getchar(); switch (choice) { case EOF: choice = 'x'; /* FALLTHROUGH */ case 'e': case 'x': case 'Q': debug_return_int(choice); default: (void) puts(_("Options are:\n" " (e)dit sudoers file again\n" " e(x)it without saving changes to sudoers file\n" " (Q)uit and save changes to sudoers file (DANGER!)\n")); } } } /* * Install signal handlers for visudo. */ static void setup_signals(void) { sigaction_t sa; debug_decl(setup_signals, SUDOERS_DEBUG_UTIL) /* * Setup signal handlers to cleanup nicely. */ memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = quit; (void) sigaction(SIGTERM, &sa, NULL); (void) sigaction(SIGHUP, &sa, NULL); (void) sigaction(SIGINT, &sa, NULL); (void) sigaction(SIGQUIT, &sa, NULL); debug_return; } static int run_command(char *path, char **argv) { int status; pid_t pid, rv; debug_decl(run_command, SUDOERS_DEBUG_UTIL) switch (pid = sudo_debug_fork()) { case -1: sudo_fatal(U_("unable to execute %s"), path); break; /* NOTREACHED */ case 0: sudo_endpwent(); sudo_endgrent(); closefrom(STDERR_FILENO + 1); execv(path, argv); sudo_warn(U_("unable to run %s"), path); _exit(127); break; /* NOTREACHED */ } do { rv = waitpid(pid, &status, 0); } while (rv == -1 && errno == EINTR); if (rv != -1) rv = WIFEXITED(status) ? WEXITSTATUS(status) : -1; debug_return_int(rv); } static bool check_owner(const char *path, bool quiet) { struct stat sb; bool ok = true; debug_decl(check_owner, SUDOERS_DEBUG_UTIL) if (stat(path, &sb) == 0) { if (sb.st_uid != sudoers_uid || sb.st_gid != sudoers_gid) { ok = false; if (!quiet) { fprintf(stderr, _("%s: wrong owner (uid, gid) should be (%u, %u)\n"), path, sudoers_uid, sudoers_gid); } } if ((sb.st_mode & 07777) != sudoers_mode) { ok = false; if (!quiet) { fprintf(stderr, _("%s: bad permissions, should be mode 0%o\n"), path, (unsigned int)sudoers_mode); } } } debug_return_bool(ok); } static bool check_syntax(const char *sudoers_file, bool quiet, bool strict, bool oldperms) { bool ok = false; debug_decl(check_syntax, SUDOERS_DEBUG_UTIL) if (strcmp(sudoers_file, "-") == 0) { sudoersin = stdin; sudoers_file = "stdin"; } else if ((sudoersin = fopen(sudoers_file, "r")) == NULL) { if (!quiet) sudo_warn(U_("unable to open %s"), sudoers_file); goto done; } init_parser(sudoers_file, quiet); if (sudoersparse() && !parse_error) { if (!quiet) sudo_warnx(U_("failed to parse %s file, unknown error"), sudoers_file); parse_error = true; errorfile = sudoers_file; } if (!parse_error) { if (!check_defaults(SETDEF_ALL, quiet) || check_aliases(strict, quiet) != 0) { parse_error = true; errorfile = NULL; } } ok = !parse_error; if (parse_error) { if (!quiet) { if (errorlineno != -1) (void) printf(_("parse error in %s near line %d\n"), errorfile, errorlineno); else if (errorfile != NULL) (void) printf(_("parse error in %s\n"), errorfile); } } else { struct sudoersfile *sp; /* Parsed OK, check mode and owner. */ if (oldperms || check_owner(sudoers_file, quiet)) { if (!quiet) (void) printf(_("%s: parsed OK\n"), sudoers_file); } else { ok = false; } TAILQ_FOREACH(sp, &sudoerslist, entries) { if (oldperms || check_owner(sp->path, quiet)) { if (!quiet) (void) printf(_("%s: parsed OK\n"), sp->path); } else { ok = false; } } } done: debug_return_bool(ok); } /* * Used to open (and lock) the initial sudoers file and to also open * any subsequent files #included via a callback from the parser. */ FILE * open_sudoers(const char *path, bool doedit, bool *keepopen) { struct sudoersfile *entry; FILE *fp; int open_flags; debug_decl(open_sudoers, SUDOERS_DEBUG_UTIL) if (checkonly) open_flags = O_RDONLY; else open_flags = O_RDWR | O_CREAT; /* Check for existing entry */ TAILQ_FOREACH(entry, &sudoerslist, entries) { if (strcmp(path, entry->path) == 0) break; } if (entry == NULL) { entry = calloc(1, sizeof(*entry)); if (entry == NULL || (entry->path = strdup(path)) == NULL) sudo_fatalx(U_("unable to allocate memory")); /* entry->modified = 0; */ entry->fd = open(entry->path, open_flags, sudoers_mode); /* entry->tpath = NULL; */ entry->doedit = doedit; if (entry->fd == -1) { sudo_warn("%s", entry->path); free(entry); debug_return_ptr(NULL); } if (!checkonly && !sudo_lock_file(entry->fd, SUDO_TLOCK)) sudo_fatalx(U_("%s busy, try again later"), entry->path); if ((fp = fdopen(entry->fd, "r")) == NULL) sudo_fatal("%s", entry->path); TAILQ_INSERT_TAIL(&sudoerslist, entry, entries); } else { /* Already exists, open .tmp version if there is one. */ if (entry->tpath != NULL) { if ((fp = fopen(entry->tpath, "r")) == NULL) sudo_fatal("%s", entry->tpath); } else { if ((fp = fdopen(entry->fd, "r")) == NULL) sudo_fatal("%s", entry->path); rewind(fp); } } if (keepopen != NULL) *keepopen = true; debug_return_ptr(fp); } /* * Look up the hostname and set user_host and user_shost. */ static void get_hostname(void) { char *p; debug_decl(get_hostname, SUDOERS_DEBUG_UTIL) if ((user_host = sudo_gethostname()) != NULL) { if ((p = strchr(user_host, '.'))) { *p = '\0'; if ((user_shost = strdup(user_host)) == NULL) sudo_fatalx(U_("unable to allocate memory")); *p = '.'; } else { user_shost = user_host; } } else { user_host = user_shost = "localhost"; } user_runhost = user_host; user_srunhost = user_shost; debug_return; } static bool alias_remove_recursive(char *name, int type) { struct member *m; struct alias *a; bool rval = true; debug_decl(alias_remove_recursive, SUDOERS_DEBUG_ALIAS) if ((a = alias_remove(name, type)) != NULL) { TAILQ_FOREACH(m, &a->members, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, type)) rval = false; } } if (rbinsert(alias_freelist, a, NULL) != 0) rval = false; } debug_return_bool(rval); } static const char * alias_type_to_string(int alias_type) { return alias_type == HOSTALIAS ? "Host_Alias" : alias_type == CMNDALIAS ? "Cmnd_Alias" : alias_type == USERALIAS ? "User_Alias" : alias_type == RUNASALIAS ? "Runas_Alias" : "Invalid_Alias"; } static int check_alias(char *name, int type, int strict, int quiet) { struct member *m; struct alias *a; int errors = 0; debug_decl(check_alias, SUDOERS_DEBUG_ALIAS) if ((a = alias_get(name, type)) != NULL) { /* check alias contents */ TAILQ_FOREACH(m, &a->members, entries) { if (m->type == ALIAS) errors += check_alias(m->name, type, strict, quiet); } alias_put(a); } else { if (!quiet) { if (errno == ELOOP) { sudo_warnx(strict ? U_("Error: cycle in %s `%s'") : U_("Warning: cycle in %s `%s'"), alias_type_to_string(type), name); } else { sudo_warnx(strict ? U_("Error: %s `%s' referenced but not defined") : U_("Warning: %s `%s' referenced but not defined"), alias_type_to_string(type), name); } } errors++; } debug_return_int(errors); } /* * Iterate through the sudoers datastructures looking for undefined * aliases or unused aliases. */ static int check_aliases(bool strict, bool quiet) { struct cmndspec *cs; struct member *m; struct privilege *priv; struct userspec *us; struct defaults *d; int atype, errors = 0; debug_decl(check_aliases, SUDOERS_DEBUG_ALIAS) alias_freelist = rbcreate(alias_compare); if (alias_freelist == NULL) { sudo_warnx(U_("unable to allocate memory")); debug_return_int(-1); } /* Forward check. */ TAILQ_FOREACH(us, &userspecs, entries) { TAILQ_FOREACH(m, &us->users, entries) { if (m->type == ALIAS) { errors += check_alias(m->name, USERALIAS, strict, quiet); } } TAILQ_FOREACH(priv, &us->privileges, entries) { TAILQ_FOREACH(m, &priv->hostlist, entries) { if (m->type == ALIAS) { errors += check_alias(m->name, HOSTALIAS, strict, quiet); } } TAILQ_FOREACH(cs, &priv->cmndlist, entries) { if (cs->runasuserlist != NULL) { TAILQ_FOREACH(m, cs->runasuserlist, entries) { if (m->type == ALIAS) { errors += check_alias(m->name, RUNASALIAS, strict, quiet); } } } if (cs->runasgrouplist != NULL) { TAILQ_FOREACH(m, cs->runasgrouplist, entries) { if (m->type == ALIAS) { errors += check_alias(m->name, RUNASALIAS, strict, quiet); } } } if ((m = cs->cmnd)->type == ALIAS) { errors += check_alias(m->name, CMNDALIAS, strict, quiet); } } } } /* Reverse check (destructive) */ TAILQ_FOREACH(us, &userspecs, entries) { TAILQ_FOREACH(m, &us->users, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, USERALIAS)) errors++; } } TAILQ_FOREACH(priv, &us->privileges, entries) { TAILQ_FOREACH(m, &priv->hostlist, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, HOSTALIAS)) errors++; } } TAILQ_FOREACH(cs, &priv->cmndlist, entries) { if (cs->runasuserlist != NULL) { TAILQ_FOREACH(m, cs->runasuserlist, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, RUNASALIAS)) errors++; } } } if (cs->runasgrouplist != NULL) { TAILQ_FOREACH(m, cs->runasgrouplist, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, RUNASALIAS)) errors++; } } } if ((m = cs->cmnd)->type == ALIAS) { if (!alias_remove_recursive(m->name, CMNDALIAS)) errors++; } } } } TAILQ_FOREACH(d, &defaults, entries) { switch (d->type) { case DEFAULTS_HOST: atype = HOSTALIAS; break; case DEFAULTS_USER: atype = USERALIAS; break; case DEFAULTS_RUNAS: atype = RUNASALIAS; break; case DEFAULTS_CMND: atype = CMNDALIAS; break; default: continue; /* not an alias */ } TAILQ_FOREACH(m, d->binding, entries) { if (m->type == ALIAS) { if (!alias_remove_recursive(m->name, atype)) errors++; } } } rbdestroy(alias_freelist, alias_free); /* If all aliases were referenced we will have an empty tree. */ if (!no_aliases() && !quiet) alias_apply(print_unused, NULL); debug_return_int(strict ? errors : 0); } static int print_unused(void *v1, void *v2) { struct alias *a = (struct alias *)v1; sudo_warnx_nodebug(U_("Warning: unused %s `%s'"), alias_type_to_string(a->type), a->name); return 0; } static void parse_sudoers_options(void) { struct plugin_info_list *plugins; debug_decl(parse_sudoers_options, SUDOERS_DEBUG_UTIL) plugins = sudo_conf_plugins(); if (plugins) { struct plugin_info *info; TAILQ_FOREACH(info, plugins, entries) { if (strcmp(info->symbol_name, "sudoers_policy") == 0) break; } if (info != NULL && info->options != NULL) { char * const *cur; #define MATCHES(s, v) (strncmp(s, v, sizeof(v) - 1) == 0) for (cur = info->options; *cur != NULL; cur++) { const char *errstr, *p; id_t id; if (MATCHES(*cur, "sudoers_file=")) { sudoers_file = *cur + sizeof("sudoers_file=") - 1; continue; } if (MATCHES(*cur, "sudoers_uid=")) { p = *cur + sizeof("sudoers_uid=") - 1; id = sudo_strtoid(p, NULL, NULL, &errstr); if (errstr == NULL) sudoers_uid = (uid_t) id; continue; } if (MATCHES(*cur, "sudoers_gid=")) { p = *cur + sizeof("sudoers_gid=") - 1; id = sudo_strtoid(p, NULL, NULL, &errstr); if (errstr == NULL) sudoers_gid = (gid_t) id; continue; } if (MATCHES(*cur, "sudoers_mode=")) { p = *cur + sizeof("sudoers_mode=") - 1; id = (id_t) sudo_strtomode(p, &errstr); if (errstr == NULL) sudoers_mode = (mode_t) id; continue; } } } } debug_return; } /* * Unlink any sudoers temp files that remain. */ static void visudo_cleanup(void) { struct sudoersfile *sp; TAILQ_FOREACH(sp, &sudoerslist, entries) { if (sp->tpath != NULL) (void) unlink(sp->tpath); } sudo_endpwent(); sudo_endgrent(); } /* * Unlink sudoers temp files (if any) and exit. */ static void quit(int signo) { struct sudoersfile *sp; struct iovec iov[4]; TAILQ_FOREACH(sp, &sudoerslist, entries) { if (sp->tpath != NULL) (void) unlink(sp->tpath); } #define emsg " exiting due to signal: " iov[0].iov_base = (char *)getprogname(); iov[0].iov_len = strlen(iov[0].iov_base); iov[1].iov_base = emsg; iov[1].iov_len = sizeof(emsg) - 1; iov[2].iov_base = strsignal(signo); iov[2].iov_len = strlen(iov[2].iov_base); iov[3].iov_base = "\n"; iov[3].iov_len = 1; ignore_result(writev(STDERR_FILENO, iov, 4)); _exit(signo); } static void usage(int fatal) { (void) fprintf(fatal ? stderr : stdout, "usage: %s [-chqsV] [-f sudoers] [-x output_file]\n", getprogname()); if (fatal) exit(1); } static void help(void) { (void) printf(_("%s - safely edit the sudoers file\n\n"), getprogname()); usage(0); (void) puts(_("\nOptions:\n" " -c, --check check-only mode\n" " -f, --file=sudoers specify sudoers file location\n" " -h, --help display help message and exit\n" " -q, --quiet less verbose (quiet) syntax error messages\n" " -s, --strict strict syntax checking\n" " -V, --version display version information and exit\n" " -x, --export=output_file write sudoers in JSON format to output_file")); exit(0); }