/* * Copyright (c) 2004-2008, 2010 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 #if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) #include #include #include #include #include #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 #if TIME_WITH_SYS_TIME # include #endif #include "sudo.h" static char *find_editor __P((int *argc_out, char ***argv_out)); extern char **NewArgv; /* XXX */ /* * Wrapper to allow users to edit privileged files with their own uid. */ int sudo_edit(argc, argv, envp) int argc; char *argv[]; char *envp[]; { ssize_t nread, nwritten; const char *tmpdir; char *cp, *suff, **nargv, *editor, **files; char **editor_argv = NULL; char buf[BUFSIZ]; int rc, i, j, ac, ofd, tfd, nargc, rval, nfiles, tmplen; int editor_argc = 0; struct stat sb; struct timeval tv, tv1, tv2; struct tempfile { char *tfile; char *ofile; struct timeval omtim; off_t osize; } *tf; /* Determine user's editor. */ editor = find_editor(&editor_argc, &editor_argv); if (editor == NULL) return 1; /* * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp */ if (stat(_PATH_VARTMP, &sb) == 0 && S_ISDIR(sb.st_mode)) tmpdir = _PATH_VARTMP; #ifdef _PATH_USRTMP else if (stat(_PATH_USRTMP, &sb) == 0 && S_ISDIR(sb.st_mode)) tmpdir = _PATH_USRTMP; #endif else tmpdir = _PATH_TMP; tmplen = strlen(tmpdir); while (tmplen > 0 && tmpdir[tmplen - 1] == '/') tmplen--; /* * For each file specified by the user, make a temporary version * and copy the contents of the original to it. */ files = argv + 1; nfiles = argc - 1; tf = emalloc2(nfiles, sizeof(*tf)); zero_bytes(tf, nfiles * sizeof(*tf)); for (i = 0, j = 0; i < nfiles; i++) { rc = -1; set_perms(PERM_RUNAS); if ((ofd = open(files[i], O_RDONLY, 0644)) != -1 || errno == ENOENT) { if (ofd == -1) { zero_bytes(&sb, sizeof(sb)); /* new file */ rc = 0; } else { #ifdef HAVE_FSTAT rc = fstat(ofd, &sb); #else rc = stat(tf[j].ofile, &sb); #endif } } set_perms(PERM_ROOT); if (rc || (ofd != -1 && !S_ISREG(sb.st_mode))) { if (rc) warning("%s", files[i]); else warningx("%s: not a regular file", files[i]); if (ofd != -1) close(ofd); continue; } tf[j].ofile = files[i]; tf[j].osize = sb.st_size; mtim_get(&sb, &tf[j].omtim); if ((cp = strrchr(tf[j].ofile, '/')) != NULL) cp++; else cp = tf[j].ofile; suff = strrchr(cp, '.'); if (suff != NULL) { easprintf(&tf[j].tfile, "%.*s/%.*sXXXXXXXX%s", tmplen, tmpdir, (int)(size_t)(suff - cp), cp, suff); } else { easprintf(&tf[j].tfile, "%.*s/%s.XXXXXXXX", tmplen, tmpdir, cp); } set_perms(PERM_USER); tfd = mkstemps(tf[j].tfile, suff ? strlen(suff) : 0); set_perms(PERM_ROOT); if (tfd == -1) { warning("mkstemps"); goto cleanup; } if (ofd != -1) { while ((nread = read(ofd, buf, sizeof(buf))) != 0) { if ((nwritten = write(tfd, buf, nread)) != nread) { if (nwritten == -1) warning("%s", tf[j].tfile); else warningx("%s: short write", tf[j].tfile); goto cleanup; } } close(ofd); } /* * We always update the stashed mtime because the time * resolution of the filesystem the temporary file is on may * not match that of the filesystem where the file to be edited * resides. It is OK if touch() fails since we only use the info * to determine whether or not a file has been modified. */ (void) touch(tfd, NULL, &tf[j].omtim); #ifdef HAVE_FSTAT rc = fstat(tfd, &sb); #else rc = stat(tf[j].tfile, &sb); #endif if (!rc) mtim_get(&sb, &tf[j].omtim); close(tfd); j++; } if ((nfiles = j) == 0) return 1; /* no files readable, you lose */ /* * Allocate space for the new argument vector and fill it in. * We concatenate the editor with its args and the file list * to create a new argv. * We allocate an extra slot to be used if execve() fails. */ nargc = editor_argc + nfiles; nargv = (char **) emalloc2(1 + nargc + 1, sizeof(char *)); nargv++; for (ac = 0; ac < editor_argc; ac++) nargv[ac] = editor_argv[ac]; for (i = 0; i < nfiles && ac < nargc; ) nargv[ac++] = tf[i++].tfile; nargv[ac] = NULL; /* * Run the editor with the invoking user's creds, * keeping track of the time spent in the editor. */ gettime(&tv1); rval = run_command(editor, nargv, envp, user_uid, TRUE); gettime(&tv2); /* Copy contents of temp files to real ones */ for (i = 0; i < nfiles; i++) { rc = -1; set_perms(PERM_USER); if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) != -1) { #ifdef HAVE_FSTAT rc = fstat(tfd, &sb); #else rc = stat(tf[i].tfile, &sb); #endif } set_perms(PERM_ROOT); if (rc || !S_ISREG(sb.st_mode)) { if (rc) warning("%s", tf[i].tfile); else warningx("%s: not a regular file", tf[i].tfile); warningx("%s left unmodified", tf[i].ofile); if (tfd != -1) close(tfd); continue; } mtim_get(&sb, &tv); if (tf[i].osize == sb.st_size && timevalcmp(&tf[i].omtim, &tv, ==)) { /* * If mtime and size match but the user spent no measurable * time in the editor we can't tell if the file was changed. */ timevalsub(&tv1, &tv2); if (timevalisset(&tv2)) { warningx("%s unchanged", tf[i].ofile); unlink(tf[i].tfile); close(tfd); continue; } } set_perms(PERM_RUNAS); ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644); set_perms(PERM_ROOT); if (ofd == -1) { warning("unable to write to %s", tf[i].ofile); warningx("contents of edit session left in %s", tf[i].tfile); close(tfd); continue; } while ((nread = read(tfd, buf, sizeof(buf))) > 0) { if ((nwritten = write(ofd, buf, nread)) != nread) { if (nwritten == -1) warning("%s", tf[i].ofile); else warningx("%s: short write", tf[i].ofile); break; } } if (nread == 0) { /* success, got EOF */ unlink(tf[i].tfile); } else if (nread < 0) { warning("unable to read temporary file"); warningx("contents of edit session left in %s", tf[i].tfile); } else { warning("unable to write to %s", tf[i].ofile); warningx("contents of edit session left in %s", tf[i].tfile); } close(ofd); } return rval; cleanup: /* Clean up temp files and return. */ for (i = 0; i < nfiles; i++) { if (tf[i].tfile != NULL) unlink(tf[i].tfile); } return 1; } static char * resolve_editor(editor, argc_out, argv_out) char *editor; int *argc_out; char ***argv_out; { char *cp, **nargv, *editor_path = NULL; int ac, nargc, wasblank; editor = estrdup(editor); /* becomes part of argv_out */ /* * Split editor into an argument vector; editor is reused (do not free). * The EDITOR and VISUAL environment variables may contain command * line args so look for those and alloc space for them too. */ nargc = 1; for (wasblank = FALSE, cp = editor; *cp != '\0'; cp++) { if (isblank((unsigned char) *cp)) wasblank = TRUE; else if (wasblank) { wasblank = FALSE; nargc++; } } /* If we can't find the editor in the user's PATH, give up. */ cp = strtok(editor, " \t"); if (cp == NULL || find_path(cp, &editor_path, NULL, getenv("PATH"), 0) != FOUND) { efree(editor); return NULL; } nargv = (char **) emalloc2(nargc + 1, sizeof(char *)); for (ac = 0; cp != NULL && ac < nargc; ac++) { nargv[ac] = cp; cp = strtok(NULL, " \t"); } nargv[ac] = NULL; *argc_out = nargc; *argv_out = nargv; return editor_path; } /* * Determine which editor to use. We don't need to worry about restricting * this to a "safe" editor since it runs with the uid of the invoking user, * not the runas (privileged) user. * Fills in argv_out with an argument vector suitable for execve() that * includes the editor with the specified files. */ static char * find_editor(argc_out, argv_out) int *argc_out; char ***argv_out; { char *cp, *editor, *editor_path = NULL, **ev, *ev0[4]; /* * If any of SUDO_EDITOR, VISUAL or EDITOR are set, choose the first one. */ ev0[0] = "SUDO_EDITOR"; ev0[1] = "VISUAL"; ev0[2] = "EDITOR"; ev0[3] = NULL; for (ev = ev0; *ev != NULL; ev++) { if ((editor = getenv(*ev)) != NULL && *editor != '\0') { editor_path = resolve_editor(editor, argc_out, argv_out); if (editor_path != NULL) break; } } if (editor_path == NULL) { /* def_editor could be a path, split it up */ editor = estrdup(def_editor); cp = strtok(editor, ":"); while (cp != NULL && editor_path == NULL) { editor_path = resolve_editor(cp, argc_out, argv_out); cp = strtok(NULL, ":"); } if (editor_path) efree(editor); } if (!editor_path) { audit_failure(NewArgv, "%s: command not found", editor); warningx("%s: command not found", editor); } return editor_path; } #else /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ /* * Must have the ability to change the effective uid to use sudoedit. */ int sudo_edit(argc, argv, envp) int argc; char *argv[]; char *envp[]; { return 1; } #endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */