Attempt to create .deps directory every time we build objects.
[doas.git] / doas.c
blob621f0cdf1086c9093fcb14f8a1795e0fe41d3c2a
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-2022 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 "parse.h"
55 #include "wrappers.h"
56 #if defined(USE_BSD_AUTH)
57 # include "bsd-auth.h"
58 #elif defined(USE_PAM)
59 # include "pam.h"
60 #elif defined(USE_SHADOW)
61 #endif
63 static inline __noreturn void usage(void)
65 fputs("usage: doas [-dnSs] [-a style] [-c command] [-C config]"
66 " [-u user] program [args]\n", stderr);
67 exit(EXIT_FAILURE);
70 static inline void print_envlist(char const *restrict const name,
71 char const *restrict const *restrict const envlist)
72 __nonnull((1));
73 static inline void print_envlist(char const *restrict const name,
74 char const *restrict const *restrict const envlist)
76 if (envlist != NULL) {
77 size_t i;
79 printf(" %s {", name);
81 for (i = 0; envlist[i] != NULL; i++) {
82 printf(" \"%s\"", envlist[i]);
84 if (envlist[i + 1] != NULL)
85 putchar(',');
88 printf(" }");
92 static __nonnull((1)) void print_rule(struct rule const *rule)
94 printf("%s", rule->permit ? "permit" : "deny");
95 print_envlist("keepenv", rule->keepenvlist);
96 print_envlist("setenv", rule->setenvlist);
97 print_envlist("unsetenv", rule->unsetenvlist);
99 if (rule->persist_time != 0)
100 printf(" persist(%lu)", (u_long)rule->persist_time);
102 if (rule->inheritenv)
103 printf(" inheritenv");
105 if (rule->nopass)
106 printf(" nopass");
108 if (rule->nolog)
109 printf(" nolog");
111 if (rule->ident.pw != NULL)
112 printf(" '%s'", rule->ident.pw->pw_name);
114 if (rule->ident.gr != NULL)
115 printf(" from '%s'", rule->ident.gr->gr_name);
117 printf(" as '%s'", rule->target.pw->pw_name);
119 if (rule->argc != 0) {
120 if (rule->argv == NULL) {
121 printf(" execute { ... }");
122 } else {
123 int i;
125 printf(" execute { ");
127 /* TODO: optimize comparation and commas. */
128 for (i = 0; rule->argv[i] != NULL; i++) {
129 assert(rule->argv[i][0] != NULL);
131 if (rule->argv[i][1] == NULL)
132 printf("\"%s\"", rule->argv[i][0]);
133 else {
134 int j = 0;
135 printf("[");
137 while (rule->argv[i][j] != NULL) {
138 printf("\"%s\"", rule->argv[i][j]);
140 if (rule->argv[i][++j] != NULL)
141 printf(", ");
144 printf("]");
147 if (rule->argv[i + 1] != NULL)
148 printf(", ");
151 if (i != rule->argc)
152 printf(" ...");
154 printf(" }");
158 printf("\n");
161 static bool match(uid_t uid, gid_t const *restrict groups, u_int ngroups,
162 uid_t target_uid, int argc, char const *const restrict *restrict argv,
163 struct rule const *restrict r)
165 if (uid != ROOT_UID)
166 if (r->ident.pw != NULL && r->ident.pw->pw_uid != uid)
167 return false;
169 if (r->ident.gr != NULL) {
170 u_int i;
171 gid_t rgid = r->ident.gr->gr_gid;
173 for (i = 0; i < ngroups && rgid != groups[i]; i++)
174 continue;
176 if (i == ngroups)
177 return false;
180 if (r->target.pw != NULL && r->target.pw->pw_uid != target_uid)
181 return false;
183 if (r->argv != NULL) {
184 int i;
186 if (r->argv[r->argc] == NULL && r->argc != argc)
187 return false;
189 /* Do not rely on r->argc since r->argv[r->argc] does not
190 point to NULL, when ellipsis used in rule
191 (in case r->argv[r->argc] != NULL && r->argc != argc). */
192 for (i = 0; r->argv[i] != NULL; i++) {
193 int j;
195 if (argv[i] == NULL)
196 return false;
198 for (j = 0; r->argv[i][j] != NULL; j++)
199 if (streq(r->argv[i][j], argv[i]))
200 break;
202 if (r->argv[i][j] == NULL)
203 return false;
207 return true;
210 static bool permit(uid_t uid, gid_t const *restrict groups, u_int ngroups,
211 uid_t target, int argc, char const *const restrict *restrict argv,
212 struct rule const *restrict *restrict lastr)
214 size_t i = get_n_rules ();
215 const struct rule *rules = get_rules ();
217 static struct rule basic_rule = {
218 /* Do not keep environ to allow user execute command
219 with clean environ. */
220 .nopass = true,
221 .nolog = true,
222 .permit = true
225 if (basic_rule.env == NULL)
226 basic_rule.env = createenv();
228 if (uid == ROOT_UID) {
229 /* But nothing else matters. */
230 /* Root is allowed to do anything (not necessarily
231 for love). */
232 *lastr = &basic_rule;
233 return true;
236 while (i-- != 0) {
237 if (match(uid, groups, ngroups, target, argc, argv, &rules[i])) {
238 *lastr = &rules[i];
239 return (*lastr)->permit;
243 if (uid == target) {
244 *lastr = &basic_rule;
245 return true;
246 } else {
247 *lastr = NULL;
248 return false;
252 static __noreturn void check_config(char const *restrict confpath,
253 int argc, char const *const restrict *restrict argv,
254 uid_t uid, gid_t const *restrict groups,
255 u_int ngroups, uid_t target)
257 struct rule const *rule;
258 int status;
260 if (setresuid(uid, uid, uid) < 0)
261 errx(EXIT_FAILURE, "unable to set uid to %lu", (u_long)uid);
263 if (parse_config(confpath) != 0)
264 exit(EXIT_FAILURE);
266 if (argc == 0)
267 exit(EXIT_SUCCESS);
269 status = permit(uid, groups, ngroups, target, argc, argv, &rule) ? EXIT_SUCCESS : EXIT_FAILURE;
271 if (rule != NULL)
272 print_rule(rule);
274 exit(status);
277 static __nonnull((1, 2, 3)) void authenticate(struct passwd *restrict original_pw, struct passwd *restrict target_pw, char *restrict login_style, bool persist)
279 static char host[HOST_NAME_MAX + 1] = { '\0' };
280 const char format[] = "\rdoas (%s@%s) password: ";
281 char prompt[256];
283 if (host[0] == '\0' && gethostname(host, sizeof(host)) < 0) {
284 host[0] = '?';
285 host[1] = '\0';
288 if (sizeof(format) - 1 + strlen(original_pw->pw_name) + strlen(host) + 1 >= sizeof(prompt))
289 strcpy(prompt, "Password: ");
290 else
291 xsnprintf(prompt, sizeof(prompt), format, original_pw->pw_name, host);
293 #if defined(USE_BSD_AUTH)
294 authuser(prompt, target_pw->pw_name, login_style, persist);
295 #elif defined(USE_PAM)
296 (void)login_style;
297 (void)persist;
298 pamauth(prompt, target_pw->pw_name, original_pw->pw_name);
299 #elif defined(USE_SHADOW)
300 (void)target_pw;
301 (void)login_style;
302 (void)persist;
303 shadowauth(prompt, original_pw->pw_name);
304 #else
305 (void)original_pw;
306 (void)target_pw;
307 (void)login_style;
308 (void)persist;
309 /* No authentication provider, only allow nopass rules. */
310 errx(EXIT_FAILURE, "no authentication module");
311 #endif
314 /* Substitute current user by user given in pw. */
315 static inline __nonnull((1)) void substitute(struct passwd const *pw)
317 #if defined(HAVE_LOGIN_CAP_H)
318 if (setusercontext(NULL, pw, pw->pw_uid, LOGIN_SETLOGINCLASS | LOGIN_SETGROUP | LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | LOGIN_SETUSER) != 0)
319 errx(EXIT_FAILURE, "failed to set user context for target");
320 #else
321 umask(022);
323 if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0)
324 err(EXIT_FAILURE, "setresgid");
326 if (initgroups(pw->pw_name, pw->pw_gid) < 0)
327 err(EXIT_FAILURE, "initgroups");
329 if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0)
330 err(EXIT_FAILURE, "setresuid");
331 #endif
334 static inline bool checkshell(char const *shell)
336 struct stat sb;
338 return shell != NULL && *shell == '/'
339 && (eaccess(shell, X_OK) == 0
340 || (stat(shell, &sb) == 0 && S_ISREG(sb.st_mode)
341 && (geteuid() != ROOT_UID || (sb.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0)));
344 static inline __returns_nonnull char *getshell(struct passwd const *target_pw)
346 size_t i;
347 char const *const shells[] = {
348 getenv("SHELL"),
349 target_pw->pw_shell,
350 _PATH_BSHELL
353 for (i = 0; i < countof(shells); i++)
354 if (checkshell(shells[i]))
355 return xstrdup(shells[i]);
357 errx(EXIT_FAILURE, "can not find shell");
360 static inline __nonnull((1)) void path_filter(char *const path)
362 char *colon = path;
363 char *s = path;
366 * Every entry in $PATH must:
367 * 1) exist
368 * 2) be directory
369 * 3) not be writable by group or other
370 * 4) be owned by root
373 while (true) {
374 struct stat sb;
375 size_t directory_length;
376 char const *directory = colon;
378 colon = strchr(colon, ':');
380 if (colon == NULL)
381 break;
383 *colon++ = '\0';
385 if (stat(directory, &sb) != 0) {
386 syslog(LOG_AUTHPRIV | LOG_NOTICE, "%s: %s", strerror(errno), directory);
387 goto lskip;
390 if (!S_ISDIR(sb.st_mode)) {
391 syslog(LOG_AUTHPRIV | LOG_NOTICE, "%s: %s", strerror(ENOTDIR), directory);
392 goto lskip;
395 if (sb.st_mode & (S_IWGRP | S_IWOTH)) {
396 syslog(LOG_AUTHPRIV | LOG_NOTICE, "%s is writable by group or other", directory);
397 goto lskip;
400 if (sb.st_uid != ROOT_UID || sb.st_gid != ROOT_UID) {
401 syslog(LOG_AUTHPRIV | LOG_NOTICE, "%s is not owned by root", directory);
402 goto lskip;
405 directory_length = colon - directory - 1;
406 memcpy(s, directory, directory_length);
407 s[directory_length] = ':';
408 s += directory_length + 1;
409 continue;
411 lskip:
412 syslog(LOG_AUTHPRIV | LOG_NOTICE, "%s is not kept in $PATH", directory);
415 s[-1] = '\0';
418 #if HAVE_FEXECVE
419 /* Returns only valid file descriptor. */
420 static inline __nonnull((1, 2)) __warn_unused_result int find_and_open_program(char const *const restrict name, char const *const restrict path)
422 int fd;
423 char *full_path;
424 size_t longest_path_size = 0;
425 size_t name_length = strlen(name);
426 char const *p1 = path;
427 char const *p2 = strchrnul(path, ':');
429 /* If program name contain '/', then do not search program in the $PATH
430 or if $PATH is not set, the default search path is implementation
431 dependent. */
432 if (strchr(name, '/') != NULL || *path == '\0') {
433 fd = safe_open(name, O_RDONLY, 0);
435 if (faccessat(fd, "", X_OK, AT_EACCESS | AT_EMPTY_PATH) != 0)
436 err(EXIT_FAILURE, "%s: file is not executable", name);
438 return fd;
441 while (true) {
442 if (p2 - p1 > longest_path_size)
443 longest_path_size = p2 - p1;
445 if (*p2 == '\0')
446 break;
448 p1 = p2 + 1;
449 p2 = strchrnul(p2 + 1, ':');
452 full_path = xmalloc(longest_path_size + 1 + name_length + 1);
453 p1 = path;
454 p2 = strchrnul(path, ':');
456 while (true) {
457 struct stat sb;
458 size_t const path_length = p2 - p1;
459 char *p = memcpy(full_path, p1, path_length);
461 p[path_length] = '/';
462 memcpy(p + path_length + 1, name, name_length + 1);
463 fd = open(full_path, O_RDONLY);
465 if (fd < 0) {
466 if (errno == ENOENT) {
467 if (*p2 == '\0')
468 break;
470 p1 = p2 + 1;
471 p2 = strchrnul(p2 + 1, ':');
472 continue;
475 err(EXIT_FAILURE, "can not open %s", full_path);
478 if (faccessat(fd, "", X_OK, AT_EACCESS | AT_EMPTY_PATH) != 0)
479 err(EXIT_FAILURE, "%s: file is not executable", full_path);
481 /* Check that the progpathname does not point to a directory. */
482 if (fstatat(fd, "", &sb, AT_EMPTY_PATH) != 0)
483 err(EXIT_FAILURE, "can not stat %s", full_path);
484 else if (S_ISDIR(sb.st_mode))
485 errc(EXIT_FAILURE, EISDIR, "%s", full_path);
487 xfree(full_path);
488 return fd;
491 err(EXIT_FAILURE, "can not find %s", name);
493 #endif
495 extern struct passwd *original_pw, *target_pw;
497 int main(int argc, char **argv)
499 char safepath[] = SAFE_PATH;
500 char *formerpath;
501 char const *confpath = NULL;
502 char *path;
503 char const *cmd = NULL;
504 char *argv0;
505 char cmdline[LINE_MAX];
506 struct rule const *rule;
507 gid_t *groups;
508 int ngroups;
509 int i, optc;
510 bool Sflag = false, sflag = false, nflag = false;
511 char *login_style = NULL;
512 char **envp;
513 #if defined(USE_TIMESTAMP)
514 int timestamp_fd = -1;
515 bool timestamp_valid = false;
516 #endif
517 #if HAVE_FEXECVE
518 int execfd;
519 char hashbang[2];
520 #endif
522 setprogname(argv[0]);
524 closefrom(STDERR_FILENO + 1);
526 if (!isatty(STDERR_FILENO))
527 exit(EXIT_FAILURE);
529 if (!isatty(STDIN_FILENO))
530 err(EXIT_FAILURE, "stdin is not a tty");
532 while ((optc = getopt(argc, argv, "+a:c:C:eLu:nSs")) >= 0) {
533 switch (optc) {
534 case 'a':
535 login_style = optarg;
536 break;
537 case 'C':
538 confpath = optarg;
539 break;
540 case 'c':
541 cmd = optarg;
542 break;
543 case 'L':
544 #if defined(USE_TIMESTAMP)
545 exit(timestamp_clear() == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
546 #elif defined(TIOCCLRVERAUTH)
547 exit((i = open(_PATH_TTY, O_RDWR)) >= 0 && ioctl(i, TIOCCLRVERAUTH) == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
548 #else
549 warn("no timestamp module");
550 exit(EXIT_SUCCESS);
551 #endif
552 case 'u':
553 target_pw = xgetpwnam(optarg);
554 break;
555 case 'n':
556 nflag = true;
557 break;
558 case 'S':
559 Sflag = true;
560 fallthrough;
561 case 's':
562 sflag = true;
563 break;
564 default:
565 usage();
569 argc -= optind;
570 argv += optind;
572 if (sflag == (cmd != NULL || confpath != NULL || argc != 0)
573 || (cmd != NULL && argc != 0))
574 usage();
576 original_pw = xgetpwuid(getuid());
578 if (target_pw == NULL)
579 target_pw = xgetpwuid(ROOT_UID);
581 /* Get number of groups. */
582 ngroups = getgroups(0, NULL);
584 if (ngroups < 0)
585 err(EXIT_FAILURE, "can not get count of groups");
587 groups = xmalloc((ngroups + 1) * sizeof(*groups));
588 ngroups = getgroups(ngroups, groups);
590 if (ngroups < 0)
591 err(EXIT_FAILURE, "can not get groups");
593 groups[ngroups++] = getgid();
595 if (cmd != NULL) {
596 argc = 3;
597 argv -= optind;
598 argv[0] = getshell(target_pw);
599 argv[1] = (char *)"-c";
600 argv[2] = (char *)cmd;
601 argv[3] = NULL;
604 if (sflag) {
605 argc = 1;
606 argv -= optind;
607 argv[0] = getshell(target_pw);
608 argv[1] = NULL;
611 if (confpath != NULL) {
612 safe_pledge("stdio rpath getpw id", NULL);
613 check_config(confpath, argc, (char const *const *)argv,
614 original_pw->pw_uid, groups, ngroups,
615 target_pw->pw_uid);
618 if (geteuid() != ROOT_UID)
619 errc(EXIT_FAILURE, EPERM, "not installed setuid");
621 check_permissions(DOAS_CONF);
623 if (parse_config(DOAS_CONF) != 0)
624 exit(EXIT_FAILURE);
626 /* cmdline is used only for logging, no need to abort
627 on truncate. */
628 (void)strlcpy(cmdline, argv[0], sizeof(cmdline));
630 for (i = 1; i < argc; i++)
631 if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline)
632 || strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
633 break;
635 if (!permit(original_pw->pw_uid, groups, ngroups, target_pw->pw_uid, argc, (char const *const *)argv, &rule)) {
636 syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed command for %s: %s",
637 original_pw->pw_name, cmdline);
638 errc(EXIT_FAILURE, EPERM, "%s", original_pw->pw_name);
641 xfree(groups);
643 if (cmd != NULL)
644 if (rule->argv != NULL)
645 errx(EXIT_FAILURE, "-c option is not allowed if arguments are specified in rule");
647 formerpath = getenv("PATH");
649 if (formerpath == NULL)
650 formerpath = (char *)"";
652 argv0 = argv[0];
654 if (Sflag)
655 argv[0] = (char *)"-doas";
657 #if defined(USE_TIMESTAMP)
658 if (rule->persist_time != 0)
659 timestamp_fd = timestamp_open(&timestamp_valid, rule->persist_time);
661 if (!rule->nopass && (timestamp_fd < 0 || !timestamp_valid)) {
662 #else
663 if (!rule->nopass) {
664 #endif
665 if (nflag)
666 errx(EXIT_FAILURE, "Authorization required");
668 authenticate(original_pw, target_pw, login_style, rule->persist_time);
669 #if defined(USE_TIMESTAMP)
670 if (timestamp_fd >= 0) {
671 timestamp_set(timestamp_fd, rule->persist_time);
672 close(timestamp_fd);
674 #endif
677 safe_pledge("stdio rpath exec getpw id", NULL);
679 substitute(target_pw);
681 safe_pledge("stdio rpath exec", NULL);
683 /* Skip logging if NOLOG is set. */
684 if (!rule->nolog) {
685 char cwdpath[PATH_MAX];
686 char const *cwd;
688 if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
689 cwd = "(failed)";
690 else
691 cwd = cwdpath;
693 syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command %s as %s from %s",
694 original_pw->pw_name, cmdline, target_pw->pw_name, cwd);
697 safe_pledge("stdio exec", NULL);
699 envp = prepenv(rule->env);
701 xfree(original_pw);
702 xfree(target_pw);
704 path = (rule->argv != NULL ? safepath : formerpath);
705 path_filter(path);
707 #if HAVE_FEXECVE
708 execfd = find_and_open_program(argv0, path);
710 if (full_read(execfd, &hashbang, 2) != 2)
711 err(EXIT_FAILURE, "can not determine whether file is script or real executable because of read() failure");
713 /* Unfortunately, we can not use close-on-execute flag with
714 scripts ran using shebang due to bug in fexecve() syscall. */
715 if (hashbang[0] != '#' || hashbang[1] != '!') {
716 int flags = fcntl(execfd, F_GETFD);
718 if (flags < 0)
719 err(EXIT_FAILURE, "can not get flags of file descriptor");
721 if (!(flags & FD_CLOEXEC) && fcntl(execfd, F_SETFD, flags | FD_CLOEXEC) != 0)
722 err(EXIT_FAILURE, "can not set close-on-execute flag");
724 #endif
726 /* setusercontext set path for the next process, so reset it for us. */
727 if (setenv("PATH", path, 1) < 0)
728 err(EXIT_FAILURE, "failed to set PATH '%s'", path);
730 #if HAVE_FEXECVE
731 fexecve(execfd, argv, envp);
732 #else
733 execvpe(argv0, argv, envp);
734 #endif
736 if (errno == ENOENT)
737 errx(EXIT_FAILURE, "%s: command not found", argv[0]);
739 err(EXIT_FAILURE, "%s", argv[0]);