Blob


1 /*
2 * Copyright (c) 2016 Martijn van Duren <vias@imperialat.at>
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 <string.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <err.h>
33 #include <unistd.h>
34 #include <pwd.h>
35 #include <grp.h>
36 #include <syslog.h>
37 #include <errno.h>
39 #include "vias.h"
41 static void __dead
42 usage(void)
43 {
44 fprintf(stderr, "usage: vias [-a style] [-C config] "
45 "file [editor args]\n");
46 exit(1);
47 }
49 size_t
50 arraylen(const char **arr)
51 {
52 size_t cnt = 0;
54 while (*arr) {
55 cnt++;
56 arr++;
57 }
58 return cnt;
59 }
61 static int
62 parseuid(const char *s, uid_t *uid)
63 {
64 struct passwd *pw;
65 const char *errstr;
67 if ((pw = getpwnam(s)) != NULL) {
68 *uid = pw->pw_uid;
69 return 0;
70 }
71 *uid = strtonum(s, 0, UID_MAX, &errstr);
72 if (errstr)
73 return -1;
74 return 0;
75 }
77 static int
78 uidcheck(const char *s, uid_t desired)
79 {
80 uid_t uid;
82 if (parseuid(s, &uid) != 0)
83 return -1;
84 if (uid != desired)
85 return -1;
86 return 0;
87 }
89 static int
90 parsegid(const char *s, gid_t *gid)
91 {
92 struct group *gr;
93 const char *errstr;
95 if ((gr = getgrnam(s)) != NULL) {
96 *gid = gr->gr_gid;
97 return 0;
98 }
99 *gid = strtonum(s, 0, GID_MAX, &errstr);
100 if (errstr)
101 return -1;
102 return 0;
105 static int
106 open_nosym(const char *file)
108 static int dep = 0;
109 struct stat sb;
110 char dir[PATH_MAX];
111 int fd, pfd;
113 dep++;
114 (void) strlcpy(dir, dirname(file), sizeof(dir));
116 if (dir[1] == '\0') {
117 dep--;
118 if ((pfd = open("/", O_CLOEXEC)) == -1)
119 return -1;
120 } else {
121 pfd = open_nosym(dir);
122 dep--;
123 if (pfd == -1)
124 return -1;
127 do {
128 if (dep) {
129 fd = openat(pfd, basename(file),
130 O_NOFOLLOW | O_CLOEXEC);
131 } else {
132 fd = openat(pfd, basename(file),
133 O_RDWR | O_NOFOLLOW | O_CLOEXEC | O_CREAT, 0777);
135 } while (fd == -1 && errno == EINTR);
136 close(pfd);
137 if (fd == -1)
138 return -1;
140 if (fstat(fd, &sb) == -1)
141 err(1, "fstat");
142 /*
143 * This should already have been checked in parse.y.
144 * It's only here to test for race-conditions
145 */
146 if (S_ISLNK(sb.st_mode))
147 errx(1, "Symbolic links not allowed");
148 if (dep && !S_ISDIR(sb.st_mode))
149 errc(1, ENOTDIR, NULL);
150 if (!dep && !S_ISREG(sb.st_mode))
151 errx(1, "File is not a regular file");
153 return fd;
156 static int
157 match(uid_t uid, gid_t *groups, int ngroups, const char *file, struct rule *r)
159 int i;
160 int flen;
162 if (r->ident[0] == ':') {
163 gid_t rgid;
164 if (parsegid(r->ident + 1, &rgid) == -1)
165 return 0;
166 for (i = 0; i < ngroups; i++) {
167 if (rgid == groups[i])
168 break;
170 if (i == ngroups)
171 return 0;
172 } else {
173 if (uidcheck(r->ident, uid) != 0)
174 return 0;
176 if (r->files != NULL) {
177 for (i = 0; r->files[i] != NULL; i++) {
178 flen = strlen(r->files[i]);
179 /* Allow access to the entire directory tree */
180 if (r->files[i][flen-1] == '/') {
181 if (!strncmp(r->files[i], file, flen))
182 break;
183 } else {
184 if (!strcmp(r->files[i], file))
185 break;
188 if (r->files[i] == NULL)
189 return 0;
191 return 1;
194 static int
195 permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr,
196 const char *file)
198 int i;
199 int fd = -1, pfd = -1;
200 uid_t suid = -1;
201 struct rule *r;
202 char *rfile;
204 if ((rfile = realpath(file, NULL)) == NULL)
205 err(1, "Unable to open %s", file);
207 *lastr = NULL;
208 for (i = 0; i < nrules; i++) {
209 r = rules[i];
210 if (match(uid, groups, ngroups, rfile, r)) {
211 /* Try to open the file, so we know the rule is really permitted */
212 if (r->target) {
213 if (parseuid(r->target, &suid) == -1)
214 err(1, "getpwnam");
215 if (seteuid(suid) == -1)
216 err(1, "seteuid");
218 if ((fd = open_nosym(rfile)) != -1) {
219 if (pfd != -1)
220 if (close(pfd) == -1)
221 err(1, "close");
222 pfd = fd;
223 *lastr = rules[i];
224 } else if (errno != EPERM) {
225 err(1, "open %s", file);
227 if (r->target && seteuid(0))
228 err(1, "seteuid");
231 if (*lastr == NULL || (*lastr)->action != PERMIT) {
232 close(fd);
233 return -1;
236 return fd;
239 static void
240 parseconfig(const char *filename, int checkperms)
242 extern FILE *yyfp;
243 extern int yyparse(void);
244 struct stat sb;
246 yyfp = fopen(filename, "r");
247 if (!yyfp)
248 err(1, checkperms ? "vias is not enabled, %s" :
249 "could not open config file %s", filename);
251 if (checkperms) {
252 if (fstat(fileno(yyfp), &sb) != 0)
253 err(1, "fstat(\"%s\")", filename);
254 if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
255 errx(1, "%s is writable by group or other", filename);
256 if (sb.st_uid != 0)
257 errx(1, "%s is not owned by root", filename);
260 yyparse();
261 fclose(yyfp);
262 if (parse_errors)
263 exit(1);
266 static void __dead
267 checkconfig(const char *confpath, uid_t uid, gid_t *groups, int ngroups,
268 const char *file)
270 struct rule *rule;
272 parseconfig(confpath, 0);
273 if (!file)
274 exit(0);
276 if (permit(uid, groups, ngroups, &rule, file) != -1) {
277 printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
278 exit(0);
280 printf("deny\n");
281 exit(1);
284 static void
285 authuser(char *myname, char *login_style, int persist)
287 char *challenge = NULL, *response, rbuf[1024], cbuf[128];
288 auth_session_t *as;
289 int fd = -1;
291 if (persist)
292 fd = open("/dev/tty", O_RDWR);
293 if (fd != -1) {
294 if (ioctl(fd, TIOCCHKVERAUTH) == 0)
295 goto good;
298 if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
299 &challenge)))
300 errx(1, "Authorization failed");
301 if (!challenge) {
302 char host[HOST_NAME_MAX + 1];
303 if (gethostname(host, sizeof(host)))
304 snprintf(host, sizeof(host), "?");
305 snprintf(cbuf, sizeof(cbuf),
306 "\rvias (%.32s@%.32s) password: ", myname, host);
307 challenge = cbuf;
309 response = readpassphrase(challenge, rbuf, sizeof(rbuf),
310 RPP_REQUIRE_TTY);
311 if (response == NULL && errno == ENOTTY) {
312 syslog(LOG_AUTHPRIV | LOG_NOTICE,
313 "tty required for %s", myname);
314 errx(1, "a tty is required");
316 if (!auth_userresponse(as, response, 0)) {
317 syslog(LOG_AUTHPRIV | LOG_NOTICE,
318 "failed auth for %s", myname);
319 errc(1, EPERM, NULL);
321 explicit_bzero(rbuf, sizeof(rbuf));
322 good:
323 if (fd != -1) {
324 int secs = 5 * 60;
325 ioctl(fd, TIOCSETVERAUTH, &secs);
326 close(fd);
329 static int
330 fcpy(int dfd, int sfd)
332 unsigned char buf[4096];
333 int r;
335 do {
336 while ((r = read(sfd, buf, sizeof(buf))) > 0) {
337 if (write(dfd, buf, r) != r)
338 return 0;
340 } while (r == -1 && errno == EINTR);
341 if (r == -1)
342 return 0;
343 return 1;
346 int
347 main(int argc, char **argv)
349 const char *confpath = NULL;
350 char tmpfile[] = "/tmp/vias.XXXXXX";
351 char myname[_PW_NAME_LEN + 1];
352 struct passwd *pw;
353 struct rule *rule;
354 uid_t uid;
355 gid_t groups[NGROUPS_MAX + 1];
356 int ngroups;
357 int i, ch;
358 int ofd, tfd;
359 char cwdpath[PATH_MAX];
360 const char *cwd;
361 char *login_style = NULL;
362 char *file;
363 char **eargv;
364 int status;
365 pid_t ret, vipid;
367 setprogname("vias");
369 closefrom(STDERR_FILENO + 1);
371 uid = getuid();
373 while ((ch = getopt(argc, argv, "a:C:")) != -1) {
374 switch (ch) {
375 case 'a':
376 login_style = optarg;
377 break;
378 case 'C':
379 confpath = optarg;
380 break;
381 default:
382 usage();
385 argv += optind;
386 argc -= optind;
388 if (argc == 0 && confpath == NULL)
389 usage();
390 file = argv[0];
391 argv++;
392 argc--;
394 pw = getpwuid(uid);
395 if (!pw)
396 err(1, "getpwuid failed");
397 if (strlcpy(myname, pw->pw_name, sizeof(myname)) >= sizeof(myname))
398 errx(1, "pw_name too long");
399 ngroups = getgroups(NGROUPS_MAX, groups);
400 if (ngroups == -1)
401 err(1, "can't get groups");
402 groups[ngroups++] = getgid();
404 if (confpath)
405 checkconfig(confpath, uid, groups, ngroups, file);
407 if (geteuid())
408 errx(1, "not installed setuid");
410 parseconfig("/etc/vias.conf", 1);
412 if (setuid(0) == -1)
413 err(1, "setuid");
414 if ((ofd = permit(uid, groups, ngroups, &rule, file)) == -1) {
415 syslog(LOG_AUTHPRIV | LOG_NOTICE,
416 "failed edit for %s: %s", myname, file);
417 err(1, "%s", file);
420 if (setreuid(uid, 0) == -1)
421 err(1, "setreuid failed");
423 if (!(rule->options & NOPASS))
424 authuser(myname, login_style, rule->options & PERSIST);
426 if (pledge("stdio rpath wpath cpath exec proc id", NULL) == -1)
427 err(1, "pledge");
429 if ((setuid(uid)) == -1)
430 err(1, "setuid failed");
432 if (pledge("stdio rpath wpath cpath exec proc", NULL) == -1)
433 err(1, "pledge");
435 if ((tfd = mkstemp(tmpfile)) == -1)
436 err(1, "mkstemp failed");
438 if (pledge("stdio rpath cpath exec proc", NULL) == -1)
439 err(1, "pledge");
441 if (!fcpy(tfd, ofd)) {
442 unlink(tmpfile);
443 err(1, "temp copy failed");
446 if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
447 cwd = "(failed)";
448 else
449 cwd = cwdpath;
451 if (pledge("stdio cpath exec proc", NULL) == -1)
452 err(1, "pledge");
454 syslog(LOG_AUTHPRIV | LOG_INFO, "%s edited %s as %s from %s",
455 myname, file, pw->pw_name, cwd);
457 if ((eargv = reallocarray(NULL, arraylen((const char **) argv) + 2,
458 sizeof(*eargv))) == NULL) {
459 unlink(tmpfile);
460 err(1, NULL);
462 eargv[0] = getenv("EDITOR");
463 if (eargv[0] == NULL || *(eargv[0]) == '\0')
464 eargv[0] = "vi";
466 switch ((vipid = fork())) {
467 case -1:
468 unlink(tmpfile);
469 err(1, "fork failed");
470 case 0:
471 if (pledge("stdio exec", NULL) == -1)
472 err(1, "pledge");
474 for (i = 0; argv[i] != NULL; i++)
475 eargv[i+1] = argv[i];
476 eargv[i+1] = tmpfile;
477 eargv[i+2] = NULL;
478 execvp(eargv[0], eargv);
479 err(1, "execvp failed");
480 default:
481 if (pledge("stdio cpath", NULL) == -1)
482 err(1, "pledge");
484 while ((ret = waitpid(vipid, &status, 0)) == -1 &&
485 errno == EINTR)
487 if (ret == -1)
488 err(1, "wait failed: Temporary file saved at %s",
489 tmpfile);
492 if (WEXITSTATUS(status) != 0) {
493 errx(1, "%s exited with status %d: Temporary file saved at %s",
494 eargv[0], WEXITSTATUS(status), tmpfile);
497 (void) lseek(tfd, 0, SEEK_SET);
498 if (ftruncate(ofd, 0) == -1)
499 err(1, "ftruncate failed: Temporary file saved at %s", tmpfile);
500 (void) lseek(ofd, 0, SEEK_SET);
501 if (!fcpy(ofd, tfd))
502 err(1, "restoring failed: Temporary file saved at %s", tmpfile);
503 if (unlink(tmpfile) == -1)
504 err(1, "unlink %s", tmpfile);
505 return 0;