Little fixes.
[doas.git] / doas.c
blobe9e6753f36446cb5eb25608dbfe52ca45056e58f
1 /* $OpenBSD: doas.c,v 1.57 2016/06/19 19:29:43 martijn Exp $ */
2 /*
3 * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org>
4 * Copyright (c) 2021 Sergey Sushilin <sergeysushilin@protonmail.com>
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 #if defined(HAVE_INTTYPES_H)
20 # include <inttypes.h>
21 #endif
23 #if _POSIX_C_SOURCE >= 200809L || _GNU_SOURCE
24 # define HAVE_FEXECVE 1
25 #else
26 # define HAVE_FEXECVE 0
27 #endif
29 #include <assert.h>
30 #include <err.h>
31 #include <errno.h>
32 #include <fcntl.h>
33 #include <limits.h>
34 #include <paths.h>
35 #include <stdint.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <sys/ioctl.h>
40 #include <sys/stat.h>
41 #include <syslog.h>
42 #include <unistd.h>
44 #if defined(HAVE_LOGIN_CAP_H)
45 # include <login_cap.h>
46 #endif /* HAVE_LOGIN_CAP_H */
48 #include "compat.h"
49 #include "env.h"
50 #include "rule.h"
51 #if USE_TIMESTAMP
52 # include "timestamp.h"
53 #endif
54 #include "wrappers.h"
56 #if defined(USE_BSD_AUTH)
57 extern __nonnull((1, 2)) void authuser(char const *restrict doas_prompt, char const *restrict name, char const *restrict login_style, bool persist);
58 #elif defined(USE_PAM)
59 extern void pamauth(char const *restrict doas_prompt, char const *restrict target_name, char const *restrict original_name);
60 #elif defined(USE_SHADOW)
61 extern __nonnull((1)) void shadowauth(char const *restrict doas_prompt, char const *name);
62 #endif
64 extern struct rule const *const rules;
65 extern size_t const nrules;
67 extern void check_permissions(char const *filename)
68 __nonnull((1));
69 extern u_int parse_config(char const *filename)
70 __nonnull((1));
72 static inline __noreturn void usage(void)
74 fputs("usage: doas [-dnSs] [-a style] [-c command] [-C config]"
75 " [-u user] program [args]\n", stderr);
76 exit(EXIT_FAILURE);
79 static __nonnull((1)) void print_rule(struct rule const *rule)
81 printf("%s", rule->permit ? "permit" : "deny");
83 if (rule->keepenvlist != NULL) {
84 char const *const *e;
86 printf(" keepenv {");
88 for (e = rule->keepenvlist; *e != NULL; e++)
89 printf(" \"%s\"%s", *e, *(e + 1) != NULL ? "," : "");
91 printf(" }");
94 if (rule->setenvlist != NULL) {
95 char const *const *e;
97 printf(" setenv {");
99 for (e = rule->setenvlist; *e != NULL; e++)
100 printf(" \"%s\"%s", *e, *(e + 1) != NULL ? "," : "");
102 printf(" }");
105 if (rule->unsetenvlist != NULL) {
106 char const *const *e;
108 printf(" unsetenv {");
110 for (e = rule->unsetenvlist; *e != NULL; e++)
111 printf(" \"%s\"%s", *e, *(e + 1) != NULL ? "," : "");
113 printf(" }");
116 if (rule->persist_time != 0)
117 printf(" persist(%lu)", (u_long)rule->persist_time);
119 if (rule->inheritenv)
120 printf(" inheritenv");
122 if (rule->nopass)
123 printf(" nopass");
125 if (rule->nolog)
126 printf(" nolog");
128 if (rule->ident.pw != NULL)
129 printf(" '%s'", rule->ident.pw->pw_name);
131 if (rule->ident.gr != NULL)
132 printf(" from '%s'", rule->ident.gr->gr_name);
134 printf(" as '%s'", rule->target.pw->pw_name);
136 if (rule->argc != 0) {
137 if (rule->argv == NULL) {
138 printf(" execute { ... }");
139 } else {
140 int i;
142 printf(" execute { ");
144 /* TODO: optimize comparation and commas. */
145 for (i = 0; rule->argv[i] != NULL; i++) {
146 assert(rule->argv[i][0] != NULL);
148 if (rule->argv[i][1] == NULL)
149 printf("\"%s\"", rule->argv[i][0]);
150 else {
151 int j = 0;
152 printf("[");
154 while (rule->argv[i][j] != NULL) {
155 printf("\"%s\"", rule->argv[i][j]);
157 if (rule->argv[i][++j] != NULL)
158 printf(", ");
161 printf("]");
164 if (rule->argv[i + 1] != NULL)
165 printf(", ");
168 if (i != rule->argc)
169 printf(" ...");
171 printf(" }");
175 printf("\n");
178 static bool match(uid_t uid, gid_t const *restrict groups, u_int ngroups,
179 uid_t target_uid, int argc, char const *const restrict *restrict argv,
180 struct rule const *restrict r)
182 if (uid != ROOT_UID)
183 if (r->ident.pw != NULL && r->ident.pw->pw_uid != uid)
184 return false;
186 if (r->ident.gr != NULL) {
187 u_int i;
188 gid_t rgid = r->ident.gr->gr_gid;
190 for (i = 0; i < ngroups && rgid != groups[i]; i++)
191 continue;
193 if (i == ngroups)
194 return false;
197 if (r->target.pw != NULL && r->target.pw->pw_uid != target_uid)
198 return false;
200 if (r->argv != NULL) {
201 int i;
203 if (r->argv[r->argc] == NULL && r->argc != argc)
204 return false;
206 /* Do not rely on r->argc since r->argv[r->argc] does not
207 point to NULL, when ellipsis used in rule
208 (in case r->argv[r->argc] != NULL && r->argc != argc). */
209 for (i = 0; r->argv[i] != NULL; i++) {
210 int j;
212 if (argv[i] == NULL)
213 return false;
215 for (j = 0; r->argv[i][j] != NULL; j++)
216 if (streq(r->argv[i][j], argv[i]))
217 break;
219 if (r->argv[i][j] == NULL)
220 return false;
224 return true;
227 static bool permit(uid_t uid, gid_t const *restrict groups, u_int ngroups,
228 uid_t target, int argc, char const *const restrict *restrict argv,
229 struct rule const *restrict *restrict lastr)
231 u_int i = nrules;
233 static struct rule basic_rule = {
234 /* Do not keep environ to allow user execute command
235 with clean environ. */
236 .nopass = true,
237 .nolog = true,
238 .permit = true
241 if (basic_rule.env == NULL)
242 basic_rule.env = createenv();
244 if (uid == ROOT_UID) {
245 /* But nothing else matters. */
246 /* Root is allowed to do anything (not necessarily
247 for love). */
248 *lastr = &basic_rule;
249 return true;
252 while (i-- != 0) {
253 if (match(uid, groups, ngroups, target, argc, argv, &rules[i])) {
254 *lastr = &rules[i];
255 return (*lastr)->permit;
259 if (uid == target) {
260 *lastr = &basic_rule;
261 return true;
262 } else {
263 *lastr = NULL;
264 return false;
268 static __noreturn void check_config(char const *restrict confpath,
269 int argc, char const *const restrict *restrict argv,
270 uid_t uid, gid_t const *restrict groups,
271 u_int ngroups, uid_t target)
273 struct rule const *rule;
274 int status;
276 if (setresuid(uid, uid, uid) < 0)
277 errx(EXIT_FAILURE, "unable to set uid to %lu", (u_long)uid);
279 if (parse_config(confpath) != 0)
280 exit(EXIT_FAILURE);
282 if (argc == 0)
283 exit(EXIT_SUCCESS);
285 status = permit(uid, groups, ngroups, target, argc, argv, &rule) ? EXIT_SUCCESS : EXIT_FAILURE;
287 if (rule != NULL)
288 print_rule(rule);
290 exit(status);
293 static __nonnull((1, 2, 3)) void authenticate(struct passwd *restrict original_pw, struct passwd *restrict target_pw, char *restrict login_style, bool persist)
295 static char host[HOST_NAME_MAX + 1] = { '\0' };
296 char format[] = "\rdoas (%s@%s) password: ";
297 char prompt[256];
299 if (host[0] == '\0' && gethostname(host, sizeof(host)) < 0) {
300 host[0] = '?';
301 host[1] = '\0';
304 if (sizeof(format) - 1 + strlen(original_pw->pw_name) + strlen(host) + 1 >= sizeof(prompt))
305 strcpy(prompt, "Password: ");
306 else
307 xsnprintf(prompt, sizeof(prompt), format, original_pw->pw_name, host);
309 #if defined(USE_BSD_AUTH)
310 authuser(prompt, target_pw->pw_name, login_style, persist);
311 #elif defined(USE_PAM)
312 (void)login_style;
313 (void)persist;
314 pamauth(prompt, target_pw->pw_name, original_pw->pw_name);
315 #elif defined(USE_SHADOW)
316 (void)target_pw;
317 (void)login_style;
318 (void)persist;
319 shadowauth(prompt, original_pw->pw_name);
320 #else
321 (void)original_pw;
322 (void)target_pw;
323 (void)login_style;
324 (void)persist;
325 /* No authentication provider, only allow nopass rules. */
326 errx(EXIT_FAILURE, "no authentication module");
327 #endif
330 /* Substitute current user by user given in pw. */
331 static inline __nonnull((1)) void substitute(struct passwd const *pw)
333 #if defined(HAVE_LOGIN_CAP_H)
334 if (setusercontext(NULL, pw, pw->pw_uid, LOGIN_SETLOGINCLASS | LOGIN_SETGROUP | LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | LOGIN_SETUSER) != 0)
335 errx(EXIT_FAILURE, "failed to set user context for target");
336 #else
337 umask(022);
339 if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0)
340 err(EXIT_FAILURE, "setresgid");
342 if (initgroups(pw->pw_name, pw->pw_gid) < 0)
343 err(EXIT_FAILURE, "initgroups");
345 if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0)
346 err(EXIT_FAILURE, "setresuid");
347 #endif
350 static inline bool checkshell(char const *shell)
352 struct stat sb;
354 return shell != NULL && *shell == '/'
355 && (eaccess(shell, X_OK) == 0
356 || (stat(shell, &sb) == 0 && S_ISREG(sb.st_mode)
357 && (geteuid() != ROOT_UID || (sb.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0)));
360 static inline __returns_nonnull char *getshell(struct passwd const *target_pw)
362 size_t i;
363 char const *const shells[] = {
364 getenv("SHELL"),
365 target_pw->pw_shell,
366 _PATH_BSHELL
369 for (i = 0; i < countof(shells); i++)
370 if (checkshell(shells[i]))
371 return xstrdup(shells[i]);
373 errx(EXIT_FAILURE, "can not find shell");
376 static inline __nonnull((1)) void path_filter(char *const path)
378 char *colon = path;
379 char *s = path;
382 * Every entry in $PATH must:
383 * 1) exist
384 * 2) be directory
385 * 3) not be writable by group or other
386 * 4) be owned by root
389 while (true) {
390 struct stat sb;
391 char const *directory = colon;
393 colon = strchr(colon, ':');
395 if (colon == NULL)
396 break;
398 *colon++ = '\0';
400 if (stat(directory, &sb) != 0) {
401 warn("%s", directory);
402 goto skip;
405 if (!S_ISDIR(sb.st_mode)) {
406 warnc(ENOTDIR, "%s", directory);
407 goto skip;
410 if (sb.st_mode & (S_IWGRP | S_IWOTH)) {
411 warnx("%s is writable by group or other", directory);
412 goto skip;
415 if (sb.st_uid != ROOT_UID || sb.st_gid != ROOT_UID) {
416 warnx("%s is not owned by root", directory);
417 goto skip;
420 size_t directory_length = colon - directory - 1;
421 memcpy(s, directory, directory_length);
422 s[directory_length] = ':';
423 s += directory_length + 1;
424 continue;
426 skip:
427 warnx("%s is not kept in $PATH", directory);
430 s[-1] = '\0';
433 #if HAVE_FEXECVE
434 /* Returns only valid file descriptor. */
435 static inline __nonnull((1, 2)) __wur int find_and_open_program(char const *const restrict name, char const *const restrict path)
437 int fd;
438 char *full_path;
439 size_t longest_path_size = 0;
440 char const *p1 = path;
441 char const *p2 = strchrnul(path, ':');
443 /* If program name contain '/', then do not search program in the $PATH
444 or if $PATH is not set, the default search path is implementation
445 dependent. */
446 if (strchr(name, '/') != NULL || *p1 == '\0') {
447 fd = safe_open(name, O_RDONLY, 0);
449 if (faccessat(fd, "", X_OK, AT_EACCESS | AT_EMPTY_PATH) != 0)
450 err(EXIT_FAILURE, "%s: file is not executable", name);
452 return fd;
455 do {
456 if (p2 - p1 > longest_path_size)
457 longest_path_size = p2 - p1;
459 p1 = p2 + 1;
460 } while (*p2 != '\0' && (p2 = strchrnul(p2 + 1, ':')) != NULL);
462 full_path = xmalloc(longest_path_size + strlen(name) + 1);
464 p1 = path;
465 p2 = strchrnul(path, ':');
467 do {
468 struct stat sb;
469 size_t const path_length = p2 - p1;
470 char *p = memcpy(full_path, p1, path_length);
472 p[path_length] = '/';
473 strcpy(p + path_length + 1, name);
475 fd = open(full_path, O_RDONLY);
477 if (fd < 0) {
478 if (errno == ENOENT)
479 continue;
480 else
481 err(EXIT_FAILURE, "can not open %s", full_path);
484 if (faccessat(fd, "", X_OK, AT_EACCESS | AT_EMPTY_PATH) != 0)
485 err(EXIT_FAILURE, "%s: file is not executable", full_path);
487 /* Check that the progpathname does not point to a directory. */
488 if (fstatat(fd, "", &sb, AT_EMPTY_PATH) != 0)
489 err(EXIT_FAILURE, "can not stat %s", full_path);
490 else if (S_ISDIR(sb.st_mode))
491 errc(EXIT_FAILURE, EISDIR, "%s", full_path);
493 xfree(full_path);
495 return fd;
496 } while (*p2 != '\0' && (p1 = p2 + 1, p2 = strchrnul(p2 + 1, ':')) != NULL);
498 err(EXIT_FAILURE, "can not find %s", name);
500 #endif
502 int main(int argc, char **argv)
504 char safepath[] = SAFE_PATH;
505 char *formerpath;
506 char const *confpath = NULL;
507 char *path;
508 char const *cmd = NULL;
509 char *argv0;
510 char cmdline[LINE_MAX];
511 struct rule const *rule;
512 gid_t *groups;
513 int ngroups;
514 int i, optc;
515 bool Sflag = false, sflag = false, nflag = false;
516 char *login_style = NULL;
517 char **envp;
518 #if defined(USE_TIMESTAMP)
519 int timestamp_fd = -1;
520 bool timestamp_valid = false;
521 #endif
522 #if HAVE_FEXECVE
523 int execfd;
524 char hashbang[2];
525 #endif
526 extern struct passwd *original_pw, *target_pw;
528 setprogname(argv[0]);
530 closefrom(STDERR_FILENO + 1);
532 if (!isatty(STDERR_FILENO))
533 exit(EXIT_FAILURE);
535 if (!isatty(STDIN_FILENO))
536 err(EXIT_FAILURE, "stdin is not a tty");
538 while ((optc = getopt(argc, argv, "+a:c:C:eLu:nSs")) >= 0) {
539 switch (optc) {
540 case 'a':
541 login_style = optarg;
542 break;
543 case 'C':
544 confpath = optarg;
545 break;
546 case 'c':
547 cmd = optarg;
548 break;
549 case 'L':
550 #if defined(USE_TIMESTAMP)
551 exit(timestamp_clear() == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
552 #elif defined(TIOCCLRVERAUTH)
553 exit((i = open(_PATH_TTY, O_RDWR)) >= 0 && ioctl(i, TIOCCLRVERAUTH) == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
554 #else
555 warn("no timestamp module");
556 exit(EXIT_SUCCESS);
557 #endif
558 case 'u':
559 target_pw = xgetpwnam(optarg);
560 break;
561 case 'n':
562 nflag = true;
563 break;
564 case 'S':
565 Sflag = true;
566 fallthrough;
567 case 's':
568 sflag = true;
569 break;
570 default:
571 usage();
575 argc -= optind;
576 argv += optind;
578 if (sflag == (cmd != NULL || confpath != NULL || argc != 0)
579 || (cmd != NULL && argc != 0))
580 usage();
582 original_pw = xgetpwuid(getuid());
584 if (target_pw == NULL)
585 target_pw = xgetpwuid(ROOT_UID);
587 /* Get number of groups. */
588 ngroups = getgroups(0, NULL);
590 if (ngroups < 0)
591 err(EXIT_FAILURE, "can not get count of groups");
593 groups = xmalloc((ngroups + 1) * sizeof(*groups));
594 ngroups = getgroups(ngroups, groups);
596 if (ngroups < 0)
597 err(EXIT_FAILURE, "can not get groups");
599 groups[ngroups++] = getgid();
601 if (cmd != NULL) {
602 argc = 3;
603 argv -= optind;
605 argv[0] = getshell(target_pw);
606 argv[1] = (char *)"-c";
607 argv[2] = (char *)cmd;
608 argv[3] = NULL;
611 if (sflag) {
612 argc = 1;
613 argv -= optind;
615 argv[0] = getshell(target_pw);
616 argv[1] = NULL;
619 if (confpath != NULL) {
620 safe_pledge("stdio rpath getpw id", NULL);
621 check_config(confpath, argc, (char const *const *)argv,
622 original_pw->pw_uid, groups, ngroups,
623 target_pw->pw_uid);
626 if (geteuid() != ROOT_UID)
627 errc(EXIT_FAILURE, EPERM, "not installed setuid");
629 check_permissions(DOAS_CONF);
631 if (parse_config(DOAS_CONF) != 0)
632 exit(EXIT_FAILURE);
634 /* cmdline is used only for logging, no need to abort
635 on truncate. */
636 (void)strlcpy(cmdline, argv[0], sizeof(cmdline));
638 for (i = 1; i < argc; i++)
639 if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline)
640 || strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
641 break;
643 if (!permit(original_pw->pw_uid, groups, ngroups, target_pw->pw_uid, argc, (char const *const *)argv, &rule)) {
644 syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed command for %s: %s",
645 original_pw->pw_name, cmdline);
646 errc(EXIT_FAILURE, EPERM, "%s", original_pw->pw_name);
649 xfree(groups);
651 if (cmd != NULL)
652 if (rule->argv != NULL)
653 errx(EXIT_FAILURE, "-c option is not allowed if arguments are specified in rule");
655 formerpath = getenv("PATH");
657 if (formerpath == NULL)
658 formerpath = (char *)"";
660 argv0 = argv[0];
662 if (Sflag)
663 argv[0] = (char *)"-doas";
665 #if defined(USE_TIMESTAMP)
666 if (rule->persist_time != 0)
667 timestamp_fd = timestamp_open(&timestamp_valid, rule->persist_time);
669 if (!rule->nopass && (timestamp_fd < 0 || !timestamp_valid)) {
670 #else
671 if (!rule->nopass) {
672 #endif
673 if (nflag)
674 errx(EXIT_FAILURE, "Authorization required");
676 authenticate(original_pw, target_pw, login_style, rule->persist_time);
677 #if defined(USE_TIMESTAMP)
678 if (timestamp_fd >= 0) {
679 timestamp_set(timestamp_fd, rule->persist_time);
680 close(timestamp_fd);
682 #endif
685 safe_pledge("stdio rpath exec getpw id", NULL);
687 substitute(target_pw);
689 safe_pledge("stdio rpath exec", NULL);
691 /* Skip logging if NOLOG is set. */
692 if (!rule->nolog) {
693 char cwdpath[PATH_MAX];
694 char const *cwd;
696 if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
697 cwd = "(failed)";
698 else
699 cwd = cwdpath;
701 syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command %s as %s from %s",
702 original_pw->pw_name, cmdline, target_pw->pw_name, cwd);
705 safe_pledge("stdio exec", NULL);
707 envp = prepenv(rule->env);
709 xfree(original_pw);
710 xfree(target_pw);
712 path = (rule->argv != NULL ? safepath : formerpath);
713 path_filter(path);
715 #if HAVE_FEXECVE
716 execfd = find_and_open_program(argv0, path);
718 if (full_read(execfd, &hashbang, 2) != 2)
719 err(EXIT_FAILURE, "can not determine whether file is script or real executable");
721 /* Unfortunately, we can not use close-on-execute flag with
722 scripts ran using shebang due to bug in fexecve() syscall. */
723 if (hashbang[0] != '#' || hashbang[1] != '!') {
724 int flags = fcntl(execfd, F_GETFD);
726 if (flags < 0)
727 err(EXIT_FAILURE, "can not get flags of file descriptor");
729 if (!(flags & FD_CLOEXEC) && fcntl(execfd, F_SETFD, flags | FD_CLOEXEC) != 0)
730 err(EXIT_FAILURE, "can not set close-on-execute flag");
732 #endif
734 /* setusercontext set path for the next process, so reset it for us. */
735 if (setenv("PATH", path, 1) < 0)
736 err(EXIT_FAILURE, "failed to set PATH '%s'", path);
738 #if HAVE_FEXECVE
739 fexecve(execfd, argv, envp);
740 #else
741 execvpe(argv0, argv, envp);
742 #endif
744 if (errno == ENOENT)
745 errx(EXIT_FAILURE, "%s: command not found", argv[0]);
747 err(EXIT_FAILURE, "%s", argv[0]);