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-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>
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 # include "bsd-auth.h"
58 #elif defined(USE_PAM)
60 #elif defined(USE_SHADOW)
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
);
70 static inline void print_envlist(char const *restrict
const name
,
71 char const *restrict
const *restrict
const envlist
)
73 static inline void print_envlist(char const *restrict
const name
,
74 char const *restrict
const *restrict
const envlist
)
76 if (envlist
!= NULL
) {
79 printf(" %s {", name
);
81 for (i
= 0; envlist
[i
] != NULL
; i
++) {
82 printf(" \"%s\"", envlist
[i
]);
84 if (envlist
[i
+ 1] != NULL
)
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");
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 { ... }");
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]);
137 while (rule
->argv
[i
][j
] != NULL
) {
138 printf("\"%s\"", rule
->argv
[i
][j
]);
140 if (rule
->argv
[i
][++j
] != NULL
)
147 if (rule
->argv
[i
+ 1] != NULL
)
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
)
166 if (r
->ident
.pw
!= NULL
&& r
->ident
.pw
->pw_uid
!= uid
)
169 if (r
->ident
.gr
!= NULL
) {
171 gid_t rgid
= r
->ident
.gr
->gr_gid
;
173 for (i
= 0; i
< ngroups
&& rgid
!= groups
[i
]; i
++)
180 if (r
->target
.pw
!= NULL
&& r
->target
.pw
->pw_uid
!= target_uid
)
183 if (r
->argv
!= NULL
) {
186 if (r
->argv
[r
->argc
] == NULL
&& r
->argc
!= argc
)
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
++) {
198 for (j
= 0; r
->argv
[i
][j
] != NULL
; j
++)
199 if (streq(r
->argv
[i
][j
], argv
[i
]))
202 if (r
->argv
[i
][j
] == NULL
)
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. */
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
232 *lastr
= &basic_rule
;
237 if (match(uid
, groups
, ngroups
, target
, argc
, argv
, &rules
[i
])) {
239 return (*lastr
)->permit
;
244 *lastr
= &basic_rule
;
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
;
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)
269 status
= permit(uid
, groups
, ngroups
, target
, argc
, argv
, &rule
) ? EXIT_SUCCESS
: EXIT_FAILURE
;
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: ";
283 if (host
[0] == '\0' && gethostname(host
, sizeof(host
)) < 0) {
288 if (sizeof(format
) - 1 + strlen(original_pw
->pw_name
) + strlen(host
) + 1 >= sizeof(prompt
))
289 strcpy(prompt
, "Password: ");
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)
298 pamauth(prompt
, target_pw
->pw_name
, original_pw
->pw_name
);
299 #elif defined(USE_SHADOW)
303 shadowauth(prompt
, original_pw
->pw_name
);
309 /* No authentication provider, only allow nopass rules. */
310 errx(EXIT_FAILURE
, "no authentication module");
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");
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");
334 static inline bool checkshell(char const *shell
)
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
)
347 char const *const shells
[] = {
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
)
366 * Every entry in $PATH must:
369 * 3) not be writable by group or other
370 * 4) be owned by root
375 size_t directory_length
;
376 char const *directory
= colon
;
378 colon
= strchr(colon
, ':');
385 if (stat(directory
, &sb
) != 0) {
386 syslog(LOG_AUTHPRIV
| LOG_NOTICE
, "%s: %s", strerror(errno
), directory
);
390 if (!S_ISDIR(sb
.st_mode
)) {
391 syslog(LOG_AUTHPRIV
| LOG_NOTICE
, "%s: %s", strerror(ENOTDIR
), directory
);
395 if (sb
.st_mode
& (S_IWGRP
| S_IWOTH
)) {
396 syslog(LOG_AUTHPRIV
| LOG_NOTICE
, "%s is writable by group or other", directory
);
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
);
405 directory_length
= colon
- directory
- 1;
406 memcpy(s
, directory
, directory_length
);
407 s
[directory_length
] = ':';
408 s
+= directory_length
+ 1;
412 syslog(LOG_AUTHPRIV
| LOG_NOTICE
, "%s is not kept in $PATH", directory
);
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
)
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
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
);
442 if (p2
- p1
> longest_path_size
)
443 longest_path_size
= p2
- p1
;
449 p2
= strchrnul(p2
+ 1, ':');
452 full_path
= xmalloc(longest_path_size
+ 1 + name_length
+ 1);
454 p2
= strchrnul(path
, ':');
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
);
466 if (errno
== ENOENT
) {
471 p2
= strchrnul(p2
+ 1, ':');
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
);
491 err(EXIT_FAILURE
, "can not find %s", name
);
495 extern struct passwd
*original_pw
, *target_pw
;
497 int main(int argc
, char **argv
)
499 char safepath
[] = SAFE_PATH
;
501 char const *confpath
= NULL
;
503 char const *cmd
= NULL
;
505 char cmdline
[LINE_MAX
];
506 struct rule
const *rule
;
510 bool Sflag
= false, sflag
= false, nflag
= false;
511 char *login_style
= NULL
;
513 #if defined(USE_TIMESTAMP)
514 int timestamp_fd
= -1;
515 bool timestamp_valid
= false;
522 setprogname(argv
[0]);
524 closefrom(STDERR_FILENO
+ 1);
526 if (!isatty(STDERR_FILENO
))
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) {
535 login_style
= optarg
;
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
);
549 warn("no timestamp module");
553 target_pw
= xgetpwnam(optarg
);
572 if (sflag
== (cmd
!= NULL
|| confpath
!= NULL
|| argc
!= 0)
573 || (cmd
!= NULL
&& argc
!= 0))
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
);
585 err(EXIT_FAILURE
, "can not get count of groups");
587 groups
= xmalloc((ngroups
+ 1) * sizeof(*groups
));
588 ngroups
= getgroups(ngroups
, groups
);
591 err(EXIT_FAILURE
, "can not get groups");
593 groups
[ngroups
++] = getgid();
598 argv
[0] = getshell(target_pw
);
599 argv
[1] = (char *)"-c";
600 argv
[2] = (char *)cmd
;
607 argv
[0] = getshell(target_pw
);
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
,
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)
626 /* cmdline is used only for logging, no need to abort
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
))
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
);
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 *)"";
655 argv
[0] = (char *)"-doas";
657 #if defined(USE_TIMESTAMP)
658 if (rule
->persist_time
!= 0)
659 timestamp_fd
= timestamp_open(×tamp_valid
, rule
->persist_time
);
661 if (!rule
->nopass
&& (timestamp_fd
< 0 || !timestamp_valid
)) {
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
);
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. */
685 char cwdpath
[PATH_MAX
];
688 if (getcwd(cwdpath
, sizeof(cwdpath
)) == NULL
)
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
);
704 path
= (rule
->argv
!= NULL
? safepath
: formerpath
);
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
);
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");
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
);
731 fexecve(execfd
, argv
, envp
);
733 execvpe(argv0
, argv
, envp
);
737 errx(EXIT_FAILURE
, "%s: command not found", argv
[0]);
739 err(EXIT_FAILURE
, "%s", argv
[0]);