1 /* $OpenBSD: doas.c,v 1.57 2016/06/19 19:29:43 martijn Exp $ */
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>
23 #if _POSIX_C_SOURCE >= 200809L || _GNU_SOURCE
24 # define HAVE_FEXECVE 1
26 # define HAVE_FEXECVE 0
39 #include <sys/ioctl.h>
44 #if defined(HAVE_LOGIN_CAP_H)
45 # include <login_cap.h>
46 #endif /* HAVE_LOGIN_CAP_H */
52 # include "timestamp.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
);
64 extern struct rule
const *const rules
;
65 extern size_t const nrules
;
67 extern void check_permissions(char const *filename
)
69 extern u_int
parse_config(char const *filename
)
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
);
79 static __nonnull((1)) void print_rule(struct rule
const *rule
)
81 printf("%s", rule
->permit
? "permit" : "deny");
83 if (rule
->keepenvlist
!= NULL
) {
88 for (e
= rule
->keepenvlist
; *e
!= NULL
; e
++)
89 printf(" \"%s\"%s", *e
, *(e
+ 1) != NULL
? "," : "");
94 if (rule
->setenvlist
!= NULL
) {
99 for (e
= rule
->setenvlist
; *e
!= NULL
; e
++)
100 printf(" \"%s\"%s", *e
, *(e
+ 1) != NULL
? "," : "");
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
? "," : "");
116 if (rule
->persist_time
!= 0)
117 printf(" persist(%lu)", (u_long
)rule
->persist_time
);
119 if (rule
->inheritenv
)
120 printf(" inheritenv");
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 { ... }");
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]);
154 while (rule
->argv
[i
][j
] != NULL
) {
155 printf("\"%s\"", rule
->argv
[i
][j
]);
157 if (rule
->argv
[i
][++j
] != NULL
)
164 if (rule
->argv
[i
+ 1] != NULL
)
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
)
183 if (r
->ident
.pw
!= NULL
&& r
->ident
.pw
->pw_uid
!= uid
)
186 if (r
->ident
.gr
!= NULL
) {
188 gid_t rgid
= r
->ident
.gr
->gr_gid
;
190 for (i
= 0; i
< ngroups
&& rgid
!= groups
[i
]; i
++)
197 if (r
->target
.pw
!= NULL
&& r
->target
.pw
->pw_uid
!= target_uid
)
200 if (r
->argv
!= NULL
) {
203 if (r
->argv
[r
->argc
] == NULL
&& r
->argc
!= argc
)
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
++) {
215 for (j
= 0; r
->argv
[i
][j
] != NULL
; j
++)
216 if (streq(r
->argv
[i
][j
], argv
[i
]))
219 if (r
->argv
[i
][j
] == NULL
)
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
)
233 static struct rule basic_rule
= {
234 /* Do not keep environ to allow user execute command
235 with clean environ. */
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
248 *lastr
= &basic_rule
;
253 if (match(uid
, groups
, ngroups
, target
, argc
, argv
, &rules
[i
])) {
255 return (*lastr
)->permit
;
260 *lastr
= &basic_rule
;
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
;
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)
285 status
= permit(uid
, groups
, ngroups
, target
, argc
, argv
, &rule
) ? EXIT_SUCCESS
: EXIT_FAILURE
;
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: ";
299 if (host
[0] == '\0' && gethostname(host
, sizeof(host
)) < 0) {
304 if (sizeof(format
) - 1 + strlen(original_pw
->pw_name
) + strlen(host
) + 1 >= sizeof(prompt
))
305 strcpy(prompt
, "Password: ");
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)
314 pamauth(prompt
, target_pw
->pw_name
, original_pw
->pw_name
);
315 #elif defined(USE_SHADOW)
319 shadowauth(prompt
, original_pw
->pw_name
);
325 /* No authentication provider, only allow nopass rules. */
326 errx(EXIT_FAILURE
, "no authentication module");
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");
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");
350 static inline bool checkshell(char const *shell
)
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
)
363 char const *const shells
[] = {
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
)
382 * Every entry in $PATH must:
385 * 3) not be writable by group or other
386 * 4) be owned by root
391 char const *directory
= colon
;
393 colon
= strchr(colon
, ':');
400 if (stat(directory
, &sb
) != 0) {
401 warn("%s", directory
);
405 if (!S_ISDIR(sb
.st_mode
)) {
406 warnc(ENOTDIR
, "%s", directory
);
410 if (sb
.st_mode
& (S_IWGRP
| S_IWOTH
)) {
411 warnx("%s is writable by group or other", directory
);
415 if (sb
.st_uid
!= ROOT_UID
|| sb
.st_gid
!= ROOT_UID
) {
416 warnx("%s is not owned by root", directory
);
420 size_t directory_length
= colon
- directory
- 1;
421 memcpy(s
, directory
, directory_length
);
422 s
[directory_length
] = ':';
423 s
+= directory_length
+ 1;
427 warnx("%s is not kept in $PATH", directory
);
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
)
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
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
);
456 if (p2
- p1
> longest_path_size
)
457 longest_path_size
= p2
- p1
;
460 } while (*p2
!= '\0' && (p2
= strchrnul(p2
+ 1, ':')) != NULL
);
462 full_path
= xmalloc(longest_path_size
+ strlen(name
) + 1);
465 p2
= strchrnul(path
, ':');
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
);
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
);
496 } while (*p2
!= '\0' && (p1
= p2
+ 1, p2
= strchrnul(p2
+ 1, ':')) != NULL
);
498 err(EXIT_FAILURE
, "can not find %s", name
);
502 int main(int argc
, char **argv
)
504 char safepath
[] = SAFE_PATH
;
506 char const *confpath
= NULL
;
508 char const *cmd
= NULL
;
510 char cmdline
[LINE_MAX
];
511 struct rule
const *rule
;
515 bool Sflag
= false, sflag
= false, nflag
= false;
516 char *login_style
= NULL
;
518 #if defined(USE_TIMESTAMP)
519 int timestamp_fd
= -1;
520 bool timestamp_valid
= false;
526 extern struct passwd
*original_pw
, *target_pw
;
528 setprogname(argv
[0]);
530 closefrom(STDERR_FILENO
+ 1);
532 if (!isatty(STDERR_FILENO
))
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) {
541 login_style
= optarg
;
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
);
555 warn("no timestamp module");
559 target_pw
= xgetpwnam(optarg
);
578 if (sflag
== (cmd
!= NULL
|| confpath
!= NULL
|| argc
!= 0)
579 || (cmd
!= NULL
&& argc
!= 0))
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
);
591 err(EXIT_FAILURE
, "can not get count of groups");
593 groups
= xmalloc((ngroups
+ 1) * sizeof(*groups
));
594 ngroups
= getgroups(ngroups
, groups
);
597 err(EXIT_FAILURE
, "can not get groups");
599 groups
[ngroups
++] = getgid();
605 argv
[0] = getshell(target_pw
);
606 argv
[1] = (char *)"-c";
607 argv
[2] = (char *)cmd
;
615 argv
[0] = getshell(target_pw
);
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
,
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)
634 /* cmdline is used only for logging, no need to abort
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
))
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
);
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 *)"";
663 argv
[0] = (char *)"-doas";
665 #if defined(USE_TIMESTAMP)
666 if (rule
->persist_time
!= 0)
667 timestamp_fd
= timestamp_open(×tamp_valid
, rule
->persist_time
);
669 if (!rule
->nopass
&& (timestamp_fd
< 0 || !timestamp_valid
)) {
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
);
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. */
693 char cwdpath
[PATH_MAX
];
696 if (getcwd(cwdpath
, sizeof(cwdpath
)) == NULL
)
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
);
712 path
= (rule
->argv
!= NULL
? safepath
: formerpath
);
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
);
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");
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
);
739 fexecve(execfd
, argv
, envp
);
741 execvpe(argv0
, argv
, envp
);
745 errx(EXIT_FAILURE
, "%s: command not found", argv
[0]);
747 err(EXIT_FAILURE
, "%s", argv
[0]);