1/*
2 * Copyright (c) 2004-2008, 2010 Todd C. Miller <Todd.Miller@courtesan.com>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17#include <config.h>
18
19#if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID)
20
21#include <sys/types.h>
22#include <sys/param.h>
23#include <sys/stat.h>
24#include <sys/time.h>
25#include <sys/wait.h>
26#include <sys/socket.h>
27#include <stdio.h>
28#ifdef STDC_HEADERS
29# include <stdlib.h>
30# include <stddef.h>
31#else
32# ifdef HAVE_STDLIB_H
33#  include <stdlib.h>
34# endif
35#endif /* STDC_HEADERS */
36#ifdef HAVE_STRING_H
37# include <string.h>
38#endif /* HAVE_STRING_H */
39#ifdef HAVE_STRINGS_H
40# include <strings.h>
41#endif /* HAVE_STRINGS_H */
42#ifdef HAVE_UNISTD_H
43# include <unistd.h>
44#endif /* HAVE_UNISTD_H */
45#include <ctype.h>
46#include <grp.h>
47#include <pwd.h>
48#include <signal.h>
49#include <errno.h>
50#include <fcntl.h>
51#if TIME_WITH_SYS_TIME
52# include <time.h>
53#endif
54
55#include "sudo.h"
56
57static char *find_editor __P((int *argc_out, char ***argv_out));
58
59extern char **NewArgv; /* XXX */
60
61/*
62 * Wrapper to allow users to edit privileged files with their own uid.
63 */
64int
65sudo_edit(argc, argv, envp)
66    int argc;
67    char *argv[];
68    char *envp[];
69{
70    ssize_t nread, nwritten;
71    const char *tmpdir;
72    char *cp, *suff, **nargv, *editor, **files;
73    char **editor_argv = NULL;
74    char buf[BUFSIZ];
75    int rc, i, j, ac, ofd, tfd, nargc, rval, nfiles, tmplen;
76    int editor_argc = 0;
77    struct stat sb;
78    struct timeval tv, tv1, tv2;
79    struct tempfile {
80	char *tfile;
81	char *ofile;
82	struct timeval omtim;
83	off_t osize;
84    } *tf = NULL;
85
86    /* Determine user's editor. */
87    editor = find_editor(&editor_argc, &editor_argv);
88    if (editor == NULL)
89	return 1;
90
91    /*
92     * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp
93     */
94    if (stat(_PATH_VARTMP, &sb) == 0 && S_ISDIR(sb.st_mode))
95	tmpdir = _PATH_VARTMP;
96#ifdef _PATH_USRTMP
97    else if (stat(_PATH_USRTMP, &sb) == 0 && S_ISDIR(sb.st_mode))
98	tmpdir = _PATH_USRTMP;
99#endif
100    else
101	tmpdir = _PATH_TMP;
102    tmplen = strlen(tmpdir);
103    while (tmplen > 0 && tmpdir[tmplen - 1] == '/')
104	tmplen--;
105
106    /*
107     * For each file specified by the user, make a temporary version
108     * and copy the contents of the original to it.
109     */
110    files = argv + 1;
111    nfiles = argc - 1;
112    tf = emalloc2(nfiles, sizeof(*tf));
113    zero_bytes(tf, nfiles * sizeof(*tf));
114    for (i = 0, j = 0; i < nfiles; i++) {
115	rc = -1;
116	set_perms(PERM_RUNAS);
117	if ((ofd = open(files[i], O_RDONLY, 0644)) != -1 || errno == ENOENT) {
118	    if (ofd == -1) {
119		zero_bytes(&sb, sizeof(sb));		/* new file */
120		rc = 0;
121	    } else {
122#ifdef HAVE_FSTAT
123		rc = fstat(ofd, &sb);
124#else
125		rc = stat(tf[j].ofile, &sb);
126#endif
127	    }
128	}
129	set_perms(PERM_ROOT);
130	if (rc || (ofd != -1 && !S_ISREG(sb.st_mode))) {
131	    if (rc)
132		warning("%s", files[i]);
133	    else
134		warningx("%s: not a regular file", files[i]);
135	    if (ofd != -1)
136		close(ofd);
137	    continue;
138	}
139	tf[j].ofile = files[i];
140	tf[j].osize = sb.st_size;
141	mtim_get(&sb, &tf[j].omtim);
142	if ((cp = strrchr(tf[j].ofile, '/')) != NULL)
143	    cp++;
144	else
145	    cp = tf[j].ofile;
146	suff = strrchr(cp, '.');
147	if (suff != NULL) {
148	    easprintf(&tf[j].tfile, "%.*s/%.*sXXXXXXXX%s", tmplen, tmpdir, (int)(size_t)(suff - cp), cp, suff);
149	} else {
150	    easprintf(&tf[j].tfile, "%.*s/%s.XXXXXXXX", tmplen, tmpdir, cp);
151	}
152	set_perms(PERM_USER);
153	tfd = mkstemps(tf[j].tfile, suff ? strlen(suff) : 0);
154	set_perms(PERM_ROOT);
155	if (tfd == -1) {
156	    warning("mkstemps");
157	    goto cleanup;
158	}
159	if (ofd != -1) {
160	    while ((nread = read(ofd, buf, sizeof(buf))) != 0) {
161		if ((nwritten = write(tfd, buf, nread)) != nread) {
162		    if (nwritten == -1)
163			warning("%s", tf[j].tfile);
164		    else
165			warningx("%s: short write", tf[j].tfile);
166		    goto cleanup;
167		}
168	    }
169	    close(ofd);
170	}
171	/*
172	 * We always update the stashed mtime because the time
173	 * resolution of the filesystem the temporary file is on may
174	 * not match that of the filesystem where the file to be edited
175	 * resides.  It is OK if touch() fails since we only use the info
176	 * to determine whether or not a file has been modified.
177	 */
178	(void) touch(tfd, NULL, &tf[j].omtim);
179#ifdef HAVE_FSTAT
180	rc = fstat(tfd, &sb);
181#else
182	rc = stat(tf[j].tfile, &sb);
183#endif
184	if (!rc)
185	    mtim_get(&sb, &tf[j].omtim);
186	close(tfd);
187	j++;
188    }
189    if ((nfiles = j) == 0)
190	return 1;			/* no files readable, you lose */
191
192    /*
193     * Allocate space for the new argument vector and fill it in.
194     * We concatenate the editor with its args and the file list
195     * to create a new argv.
196     * We allocate an extra slot to be used if execve() fails.
197     */
198    nargc = editor_argc + nfiles;
199    nargv = (char **) emalloc2(1 + nargc + 1, sizeof(char *));
200    nargv++;
201    for (ac = 0; ac < editor_argc; ac++)
202	nargv[ac] = editor_argv[ac];
203    for (i = 0; i < nfiles && ac < nargc; )
204	nargv[ac++] = tf[i++].tfile;
205    nargv[ac] = NULL;
206
207    /*
208     * Run the editor with the invoking user's creds,
209     * keeping track of the time spent in the editor.
210     */
211    gettime(&tv1);
212    rval = run_command(editor, nargv, envp, user_uid, TRUE);
213    gettime(&tv2);
214
215    /* Copy contents of temp files to real ones */
216    for (i = 0; i < nfiles; i++) {
217	rc = -1;
218	set_perms(PERM_USER);
219	if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) != -1) {
220#ifdef HAVE_FSTAT
221	    rc = fstat(tfd, &sb);
222#else
223	    rc = stat(tf[i].tfile, &sb);
224#endif
225	}
226	set_perms(PERM_ROOT);
227	if (rc || !S_ISREG(sb.st_mode)) {
228	    if (rc)
229		warning("%s", tf[i].tfile);
230	    else
231		warningx("%s: not a regular file", tf[i].tfile);
232	    warningx("%s left unmodified", tf[i].ofile);
233	    if (tfd != -1)
234		close(tfd);
235	    continue;
236	}
237	mtim_get(&sb, &tv);
238	if (tf[i].osize == sb.st_size && timevalcmp(&tf[i].omtim, &tv, ==)) {
239	    /*
240	     * If mtime and size match but the user spent no measurable
241	     * time in the editor we can't tell if the file was changed.
242	     */
243	    timevalsub(&tv1, &tv2);
244	    if (timevalisset(&tv2)) {
245		warningx("%s unchanged", tf[i].ofile);
246		unlink(tf[i].tfile);
247		close(tfd);
248		continue;
249	    }
250	}
251	set_perms(PERM_RUNAS);
252	ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644);
253	set_perms(PERM_ROOT);
254	if (ofd == -1) {
255	    warning("unable to write to %s", tf[i].ofile);
256	    warningx("contents of edit session left in %s", tf[i].tfile);
257	    close(tfd);
258	    continue;
259	}
260	while ((nread = read(tfd, buf, sizeof(buf))) > 0) {
261	    if ((nwritten = write(ofd, buf, nread)) != nread) {
262		if (nwritten == -1)
263		    warning("%s", tf[i].ofile);
264		else
265		    warningx("%s: short write", tf[i].ofile);
266		break;
267	    }
268	}
269	if (nread == 0) {
270	    /* success, got EOF */
271	    unlink(tf[i].tfile);
272	} else if (nread < 0) {
273	    warning("unable to read temporary file");
274	    warningx("contents of edit session left in %s", tf[i].tfile);
275	} else {
276	    warning("unable to write to %s", tf[i].ofile);
277	    warningx("contents of edit session left in %s", tf[i].tfile);
278	}
279	close(ofd);
280    }
281
282    return rval;
283cleanup:
284    /* Clean up temp files and return. */
285    if (tf != NULL) {
286	for (i = 0; i < nfiles; i++) {
287	    if (tf[i].tfile != NULL)
288		unlink(tf[i].tfile);
289	}
290    }
291    return 1;
292}
293
294static char *
295resolve_editor(editor, argc_out, argv_out)
296    char *editor;
297    int *argc_out;
298    char ***argv_out;
299{
300    char *cp, **nargv, *editor_path = NULL;
301    int ac, nargc, wasblank;
302
303    editor = estrdup(editor); /* becomes part of argv_out */
304
305    /*
306     * Split editor into an argument vector; editor is reused (do not free).
307     * The EDITOR and VISUAL environment variables may contain command
308     * line args so look for those and alloc space for them too.
309     */
310    nargc = 1;
311    for (wasblank = FALSE, cp = editor; *cp != '\0'; cp++) {
312	if (isblank((unsigned char) *cp))
313	    wasblank = TRUE;
314	else if (wasblank) {
315	    wasblank = FALSE;
316	    nargc++;
317	}
318    }
319    /* If we can't find the editor in the user's PATH, give up. */
320    cp = strtok(editor, " \t");
321    if (cp == NULL ||
322	find_path(cp, &editor_path, NULL, getenv("PATH"), 0) != FOUND) {
323	efree(editor);
324	return NULL;
325    }
326    nargv = (char **) emalloc2(nargc + 1, sizeof(char *));
327    for (ac = 0; cp != NULL && ac < nargc; ac++) {
328	nargv[ac] = cp;
329	cp = strtok(NULL, " \t");
330    }
331    nargv[ac] = NULL;
332
333    *argc_out = nargc;
334    *argv_out = nargv;
335    return editor_path;
336}
337
338/*
339 * Determine which editor to use.  We don't need to worry about restricting
340 * this to a "safe" editor since it runs with the uid of the invoking user,
341 * not the runas (privileged) user.
342 * Fills in argv_out with an argument vector suitable for execve() that
343 * includes the editor with the specified files.
344 */
345static char *
346find_editor(argc_out, argv_out)
347    int *argc_out;
348    char ***argv_out;
349{
350    char *cp, *editor, *editor_path = NULL, **ev, *ev0[4];
351
352    /*
353     * If any of SUDO_EDITOR, VISUAL or EDITOR are set, choose the first one.
354     */
355    ev0[0] = "SUDO_EDITOR";
356    ev0[1] = "VISUAL";
357    ev0[2] = "EDITOR";
358    ev0[3] = NULL;
359    for (ev = ev0; *ev != NULL; ev++) {
360	if ((editor = getenv(*ev)) != NULL && *editor != '\0') {
361	    editor_path = resolve_editor(editor, argc_out, argv_out);
362	    if (editor_path != NULL)
363		break;
364	}
365    }
366    if (editor_path == NULL) {
367	/* def_editor could be a path, split it up */
368	editor = estrdup(def_editor);
369	cp = strtok(editor, ":");
370	while (cp != NULL && editor_path == NULL) {
371	    editor_path = resolve_editor(cp, argc_out, argv_out);
372	    cp = strtok(NULL, ":");
373	}
374	if (editor_path)
375	    efree(editor);
376    }
377    if (!editor_path) {
378	audit_failure(NewArgv, "%s: command not found", editor);
379	warningx("%s: command not found", editor);
380    }
381    return editor_path;
382}
383
384#else /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */
385
386/*
387 * Must have the ability to change the effective uid to use sudoedit.
388 */
389int
390sudo_edit(argc, argv, envp)
391    int argc;
392    char *argv[];
393    char *envp[];
394{
395    return 1;
396}
397
398#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */
399