Blob


1 /*
2 * Copyright (c) 2016 Martijn van Duren <martijn@openbsd.org>
3 * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
18 #include <sys/types.h>
19 #include <sys/stat.h>
20 #include <sys/wait.h>
21 #include <sys/ioctl.h>
23 #include <fcntl.h>
24 #include <limits.h>
25 #include <libgen.h>
26 #include <login_cap.h>
27 #include <bsd_auth.h>
28 #include <readpassphrase.h>
29 #include <signal.h>
30 #include <string.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <err.h>
34 #include <unistd.h>
35 #include <pwd.h>
36 #include <grp.h>
37 #include <syslog.h>
38 #include <errno.h>
40 #include "vias.h"
42 static void __dead
43 usage(void)
44 {
45 fprintf(stderr, "usage: vias [-a style] [-C config] "
46 "file [editor args]\n");
47 exit(1);
48 }
50 size_t
51 arraylen(const char * const*arr)
52 {
53 size_t cnt = 0;
55 while (*arr) {
56 cnt++;
57 arr++;
58 }
59 return cnt;
60 }
62 static int
63 parseuid(const char *s, uid_t *uid)
64 {
65 struct passwd *pw;
66 const char *errstr;
68 if ((pw = getpwnam(s)) != NULL) {
69 *uid = pw->pw_uid;
70 return 0;
71 }
72 *uid = strtonum(s, 0, UID_MAX, &errstr);
73 if (errstr)
74 return -1;
75 return 0;
76 }
78 static int
79 uidcheck(const char *s, uid_t desired)
80 {
81 uid_t uid;
83 if (parseuid(s, &uid) != 0)
84 return -1;
85 if (uid != desired)
86 return -1;
87 return 0;
88 }
90 static int
91 parsegid(const char *s, gid_t *gid)
92 {
93 struct group *gr;
94 const char *errstr;
96 if ((gr = getgrnam(s)) != NULL) {
97 *gid = gr->gr_gid;
98 return 0;
99 }
100 *gid = strtonum(s, 0, GID_MAX, &errstr);
101 if (errstr)
102 return -1;
103 return 0;
106 static int
107 open_nosym(const char *file)
109 static int dep = 0;
110 struct stat sb;
111 char path[PATH_MAX];
112 int fd, pfd;
114 dep++;
115 strlcpy(path, file, sizeof(path));
116 (void) strlcpy(path, dirname(path), sizeof(path));
118 if (path[1] == '\0') {
119 dep--;
120 if ((pfd = open("/", O_CLOEXEC)) == -1)
121 return -1;
122 } else {
123 pfd = open_nosym(path);
124 dep--;
125 if (pfd == -1)
126 return -1;
129 do {
130 strlcpy(path, file, sizeof(path));
131 if (dep) {
132 fd = openat(pfd, basename(path),
133 O_NOFOLLOW | O_CLOEXEC);
134 } else {
135 fd = openat(pfd, basename(path),
136 O_RDWR | O_NOFOLLOW | O_CLOEXEC | O_CREAT, 0777);
138 } while (fd == -1 && errno == EINTR);
139 close(pfd);
140 if (fd == -1)
141 return -1;
143 if (fstat(fd, &sb) == -1)
144 err(1, "fstat");
145 /*
146 * This should already have been checked in parse.y.
147 * It's only here to test for race-conditions
148 */
149 if (S_ISLNK(sb.st_mode))
150 errx(1, "Symbolic links not allowed");
151 if (dep && !S_ISDIR(sb.st_mode))
152 errc(1, ENOTDIR, NULL);
153 if (!dep && !S_ISREG(sb.st_mode))
154 errx(1, "File is not a regular file");
156 return fd;
159 static int
160 match(uid_t uid, gid_t *groups, int ngroups, const char *file, struct rule *r)
162 int i;
163 int flen;
165 if (r->ident[0] == ':') {
166 gid_t rgid;
167 if (parsegid(r->ident + 1, &rgid) == -1)
168 return 0;
169 for (i = 0; i < ngroups; i++) {
170 if (rgid == groups[i])
171 break;
173 if (i == ngroups)
174 return 0;
175 } else {
176 if (uidcheck(r->ident, uid) != 0)
177 return 0;
179 if (r->files != NULL) {
180 for (i = 0; r->files[i] != NULL; i++) {
181 flen = strlen(r->files[i]);
182 /* Allow access to the entire directory tree */
183 if (r->files[i][flen-1] == '/') {
184 if (!strncmp(r->files[i], file, flen))
185 break;
186 } else {
187 if (!strcmp(r->files[i], file))
188 break;
191 if (r->files[i] == NULL)
192 return 0;
194 return 1;
197 static int
198 permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr,
199 const char *file)
201 int i;
202 int fd = -1, pfd = -1;
203 uid_t suid = -1;
204 struct rule *r;
205 char *rfile;
207 if ((rfile = realpath(file, NULL)) == NULL)
208 err(1, "Unable to open %s", file);
210 *lastr = NULL;
211 for (i = 0; i < nrules; i++) {
212 r = rules[i];
213 if (match(uid, groups, ngroups, rfile, r)) {
214 /* Try to open the file, so we know the rule is really permitted */
215 if (r->target) {
216 if (parseuid(r->target, &suid) == -1)
217 err(1, "getpwnam");
218 if (seteuid(suid) == -1)
219 err(1, "seteuid");
221 if ((fd = open_nosym(rfile)) != -1) {
222 if (pfd != -1)
223 if (close(pfd) == -1)
224 err(1, "close");
225 pfd = fd;
226 *lastr = rules[i];
227 } else if (errno != EPERM) {
228 err(1, "open %s", file);
230 if (r->target && seteuid(0))
231 err(1, "seteuid");
234 if (*lastr == NULL || (*lastr)->action != PERMIT) {
235 if (fd != -1 && close(fd) == -1)
236 err(1, "close");
237 return -1;
240 return fd;
243 static void
244 parseconfig(const char *filename, int checkperms)
246 extern FILE *yyfp;
247 extern int yyparse(void);
248 struct stat sb;
250 yyfp = fopen(filename, "r");
251 if (!yyfp)
252 err(1, checkperms ? "vias is not enabled, %s" :
253 "could not open config file %s", filename);
255 if (checkperms) {
256 if (fstat(fileno(yyfp), &sb) != 0)
257 err(1, "fstat(\"%s\")", filename);
258 if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
259 errx(1, "%s is writable by group or other", filename);
260 if (sb.st_uid != 0)
261 errx(1, "%s is not owned by root", filename);
264 yyparse();
265 fclose(yyfp);
266 if (parse_errors)
267 exit(1);
270 static void __dead
271 checkconfig(const char *confpath, uid_t uid, gid_t *groups, int ngroups,
272 const char *file)
274 struct rule *rule;
276 parseconfig(confpath, 0);
277 if (!file)
278 exit(0);
280 if (permit(uid, groups, ngroups, &rule, file) != -1) {
281 printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
282 exit(0);
284 printf("deny\n");
285 exit(1);
288 static void
289 authuser(char *myname, char *login_style, int persist)
291 char *challenge = NULL, *response, rbuf[1024], cbuf[128];
292 auth_session_t *as;
293 int fd = -1;
295 if (persist)
296 fd = open("/dev/tty", O_RDWR);
297 if (fd != -1) {
298 if (ioctl(fd, TIOCCHKVERAUTH) == 0)
299 goto good;
302 if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
303 &challenge)))
304 errx(1, "Authorization failed");
305 if (!challenge) {
306 char host[HOST_NAME_MAX + 1];
307 if (gethostname(host, sizeof(host)))
308 snprintf(host, sizeof(host), "?");
309 snprintf(cbuf, sizeof(cbuf),
310 "\rvias (%.32s@%.32s) password: ", myname, host);
311 challenge = cbuf;
313 response = readpassphrase(challenge, rbuf, sizeof(rbuf),
314 RPP_REQUIRE_TTY);
315 if (response == NULL && errno == ENOTTY) {
316 syslog(LOG_AUTHPRIV | LOG_NOTICE,
317 "tty required for %s", myname);
318 errx(1, "a tty is required");
320 if (!auth_userresponse(as, response, 0)) {
321 explicit_bzero(rbuf, sizeof(rbuf));
322 syslog(LOG_AUTHPRIV | LOG_NOTICE,
323 "failed auth for %s", myname);
324 errc(1, EPERM, NULL);
326 explicit_bzero(rbuf, sizeof(rbuf));
327 good:
328 if (fd != -1) {
329 int secs = 5 * 60;
330 ioctl(fd, TIOCSETVERAUTH, &secs);
331 close(fd);
334 static int
335 fcpy(int dfd, int sfd)
337 unsigned char buf[4096];
338 int r;
340 do {
341 while ((r = read(sfd, buf, sizeof(buf))) > 0) {
342 if (write(dfd, buf, r) != r)
343 return 0;
345 } while (r == -1 && errno == EINTR);
346 if (r == -1)
347 return 0;
348 return 1;
351 int
352 main(int argc, char **argv)
354 const char *confpath = NULL;
355 char tmpfile[] = "/tmp/vias.XXXXXX";
356 char myname[_PW_NAME_LEN + 1];
357 struct passwd *pw;
358 struct rule *rule;
359 uid_t uid;
360 gid_t groups[NGROUPS_MAX + 1];
361 int ngroups;
362 int i, ch;
363 int ofd, tfd, ttyfd;
364 char cwdpath[PATH_MAX];
365 const char *cwd;
366 char *login_style = NULL;
367 char *file;
368 char **eargv;
369 int status;
370 pid_t ret, vipid;
372 setprogname("vias");
374 closefrom(STDERR_FILENO + 1);
376 uid = getuid();
378 while ((ch = getopt(argc, argv, "a:C:")) != -1) {
379 switch (ch) {
380 case 'a':
381 login_style = optarg;
382 break;
383 case 'C':
384 confpath = optarg;
385 break;
386 default:
387 usage();
390 argv += optind;
391 argc -= optind;
393 if (argc == 0 && confpath == NULL)
394 usage();
395 file = argv[0];
396 argv++;
397 argc--;
399 pw = getpwuid(uid);
400 if (!pw)
401 err(1, "getpwuid failed");
402 if (strlcpy(myname, pw->pw_name, sizeof(myname)) >= sizeof(myname))
403 errx(1, "pw_name too long");
404 ngroups = getgroups(NGROUPS_MAX, groups);
405 if (ngroups == -1)
406 err(1, "can't get groups");
407 groups[ngroups++] = getgid();
409 if (confpath)
410 checkconfig(confpath, uid, groups, ngroups, file);
412 if (geteuid())
413 errx(1, "not installed setuid");
415 parseconfig("/etc/vias.conf", 1);
417 if (setuid(0) == -1)
418 err(1, "setuid");
419 if ((ofd = permit(uid, groups, ngroups, &rule, file)) == -1) {
420 syslog(LOG_AUTHPRIV | LOG_NOTICE,
421 "failed edit for %s: %s", myname, file);
422 errc(1, EPERM, "%s", file);
425 if (setreuid(uid, 0) == -1)
426 err(1, "setreuid failed");
428 if (!(rule->options & NOPASS))
429 authuser(myname, login_style, rule->options & PERSIST);
431 if (pledge("stdio rpath wpath cpath exec proc id tty", NULL) == -1)
432 err(1, "pledge");
434 if ((setuid(uid)) == -1)
435 err(1, "setuid failed");
437 if (pledge("stdio rpath wpath cpath exec proc tty", NULL) == -1)
438 err(1, "pledge");
440 if ((tfd = mkstemp(tmpfile)) == -1)
441 err(1, "mkstemp failed");
443 if (pledge("stdio rpath cpath exec proc tty", NULL) == -1)
444 err(1, "pledge");
446 if (!fcpy(tfd, ofd)) {
447 unlink(tmpfile);
448 err(1, "temp copy failed");
451 if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
452 cwd = "(failed)";
453 else
454 cwd = cwdpath;
456 if (pledge("stdio cpath exec proc tty", NULL) == -1)
457 err(1, "pledge");
459 syslog(LOG_AUTHPRIV | LOG_INFO, "%s edited %s from %s",
460 myname, file, cwd);
462 if ((eargv = reallocarray(NULL, arraylen((const char * const*)argv) + 2,
463 sizeof(*eargv))) == NULL) {
464 unlink(tmpfile);
465 err(1, NULL);
467 eargv[0] = getenv("EDITOR");
468 if (eargv[0] == NULL || *(eargv[0]) == '\0')
469 eargv[0] = "vi";
471 switch ((vipid = fork())) {
472 case -1:
473 unlink(tmpfile);
474 err(1, "fork failed");
475 case 0:
476 if (pledge("stdio exec", NULL) == -1)
477 err(1, "pledge");
479 for (i = 0; argv[i] != NULL; i++)
480 eargv[i+1] = argv[i];
481 eargv[i+1] = tmpfile;
482 eargv[i+2] = NULL;
483 execvp(eargv[0], eargv);
484 err(1, "execvp failed");
485 default:
486 if (pledge("stdio cpath proc tty", NULL) == -1)
487 err(1, "pledge");
489 (void)setpgid(vipid, 0);
490 ttyfd = open("/dev/tty", O_RDWR);
491 if (ttyfd != -1)
492 (void)tcsetpgrp(ttyfd, vipid);
493 while ((ret = waitpid(vipid, &status, 0)) == -1 &&
494 errno == EINTR)
496 if (ret == -1)
497 err(1, "wait failed: Temporary file saved at %s",
498 tmpfile);
501 if (WEXITSTATUS(status) != 0) {
502 errx(1, "%s exited with status %d: Temporary file saved at %s",
503 eargv[0], WEXITSTATUS(status), tmpfile);
506 (void) lseek(tfd, 0, SEEK_SET);
507 if (ftruncate(ofd, 0) == -1)
508 err(1, "ftruncate failed: Temporary file saved at %s", tmpfile);
509 (void) lseek(ofd, 0, SEEK_SET);
510 if (!fcpy(ofd, tfd))
511 err(1, "restoring failed: Temporary file saved at %s", tmpfile);
512 if (unlink(tmpfile) == -1)
513 err(1, "unlink %s", tmpfile);
514 return 0;