]> git.armaanb.net Git - opendoas.git/blob - doas.c
sudo emulation: if execvpe fails with ENOENT, print "command not found"
[opendoas.git] / doas.c
1 /* $OpenBSD: doas.c,v 1.9 2015/07/18 18:44:26 tedu Exp $ */
2 /*
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  */
17
18 #include <sys/types.h>
19 #include <sys/stat.h>
20
21 #include <limits.h>
22 #include <login_cap.h>
23 #include <bsd_auth.h>
24 #include <string.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <err.h>
28 #include <unistd.h>
29 #include <pwd.h>
30 #include <grp.h>
31 #include <syslog.h>
32 #include <errno.h>
33
34 #include "doas.h"
35
36 static void __dead
37 usage(void)
38 {
39         fprintf(stderr, "usage: doas [-s] [-u user] command [args]\n");
40         exit(1);
41 }
42
43 size_t
44 arraylen(const char **arr)
45 {
46         size_t cnt = 0;
47
48         while (*arr) {
49                 cnt++;
50                 arr++;
51         }
52         return cnt;
53 }
54
55 static int
56 parseuid(const char *s, uid_t *uid)
57 {
58         struct passwd *pw;
59         const char *errstr;
60
61         if ((pw = getpwnam(s)) != NULL) {
62                 *uid = pw->pw_uid;
63                 return 0;
64         }
65         *uid = strtonum(s, 0, UID_MAX, &errstr);
66         if (errstr)
67                 return -1;
68         return 0;
69 }
70
71 static int
72 uidcheck(const char *s, uid_t desired)
73 {
74         uid_t uid;
75
76         if (parseuid(s, &uid) != 0)
77                 return -1;
78         if (uid != desired)
79                 return -1;
80         return 0;
81 }
82
83 static gid_t
84 strtogid(const char *s)
85 {
86         struct group *gr;
87         const char *errstr;
88         gid_t gid;
89
90         if ((gr = getgrnam(s)) != NULL)
91                 return gr->gr_gid;
92         gid = strtonum(s, 0, GID_MAX, &errstr);
93         if (errstr)
94                 return -1;
95         return gid;
96 }
97
98 static int
99 match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd,
100     struct rule *r)
101 {
102         int i;
103
104         if (r->ident[0] == ':') {
105                 gid_t rgid = strtogid(r->ident + 1);
106                 if (rgid == -1)
107                         return 0;
108                 for (i = 0; i < ngroups; i++) {
109                         if (rgid == groups[i])
110                                 break;
111                 }
112                 if (i == ngroups)
113                         return 0;
114         } else {
115                 if (uidcheck(r->ident, uid) != 0)
116                         return 0;
117         }
118         if (r->target && uidcheck(r->target, target) != 0)
119                 return 0;
120         if (r->cmd && strcmp(r->cmd, cmd) != 0)
121                 return 0;
122         return 1;
123 }
124
125 static int
126 permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr,
127     uid_t target, const char *cmd)
128 {
129         int i;
130
131         *lastr = NULL;
132         for (i = 0; i < nrules; i++) {
133                 if (match(uid, groups, ngroups, target, cmd, rules[i]))
134                         *lastr = rules[i];
135         }
136         if (!*lastr)
137                 return 0;
138         return (*lastr)->action == PERMIT;
139 }
140
141 static void
142 parseconfig(const char *filename)
143 {
144         extern FILE *yyfp;
145         extern int yyparse(void);
146         struct stat sb;
147
148         yyfp = fopen(filename, "r");
149         if (!yyfp) {
150                 fprintf(stderr, "doas is not enabled.\n");
151                 exit(1);
152         }
153
154         if (fstat(fileno(yyfp), &sb) != 0)
155                 err(1, "fstat(\"%s\")", filename);
156         if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
157                 errx(1, "%s is writable by group or other", filename);
158         if (sb.st_uid != 0)
159                 errx(1, "%s is not owned by root", filename);
160
161         yyparse();
162         fclose(yyfp);
163 }
164
165 static int
166 copyenvhelper(const char **oldenvp, const char **safeset, int nsafe,
167     char **envp, int ei)
168 {
169         int i;
170
171         for (i = 0; i < nsafe; i++) {
172                 const char **oe = oldenvp;
173                 while (*oe) {
174                         size_t len = strlen(safeset[i]);
175                         if (strncmp(*oe, safeset[i], len) == 0 &&
176                             (*oe)[len] == '=') {
177                                 if (!(envp[ei++] = strdup(*oe)))
178                                         err(1, "strdup");
179                                 break;
180                         }
181                         oe++;
182                 }
183         }
184         return ei;
185 }
186
187 static char **
188 copyenv(const char **oldenvp, struct rule *rule)
189 {
190         const char *safeset[] = {
191                 "DISPLAY", "HOME", "LOGNAME", "MAIL", "SHELL",
192                 "PATH", "TERM", "USER", "USERNAME",
193                 NULL,
194         };
195         char **envp;
196         const char **extra;
197         int ei;
198         int i, j;
199         int nsafe;
200         int nextras = 0;
201         
202         if ((rule->options & KEEPENV) && !rule->envlist) {
203                 j = arraylen(oldenvp);
204                 envp = reallocarray(NULL, j + 1, sizeof(char *));
205                 if (!envp)
206                         err(1, "reallocarray");
207                 for (i = 0; i < j; i++) {
208                         if (!(envp[i] = strdup(oldenvp[i])))
209                                 err(1, "strdup");
210                 }
211                 envp[i] = NULL;
212                 return envp;
213         }
214
215         nsafe = arraylen(safeset);
216         if ((extra = rule->envlist)) {
217                 nextras = arraylen(extra);
218                 for (i = 0; i < nsafe; i++) {
219                         for (j = 0; j < nextras; j++) {
220                                 if (strcmp(extra[j], safeset[i]) == 0) {
221                                         extra[j--] = extra[nextras--];
222                                         extra[nextras] = NULL;
223                                 }
224                         }
225                 }
226         }
227
228         envp = reallocarray(NULL, nsafe + nextras + 1, sizeof(char *));
229         if (!envp)
230                 err(1, "can't allocate new environment");
231
232         ei = 0;
233         ei = copyenvhelper(oldenvp, safeset, nsafe, envp, ei);
234         ei = copyenvhelper(oldenvp, rule->envlist, nextras, envp, ei);
235         envp[ei] = NULL;
236
237         return envp;
238 }
239
240 static void __dead
241 fail(void)
242 {
243         fprintf(stderr, "Permission denied\n");
244         exit(1);
245 }
246
247 int
248 main(int argc, char **argv, char **envp)
249 {
250         const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:"
251             "/usr/local/bin:/usr/local/sbin";
252         char *shargv[] = { NULL, NULL };
253         char *sh;
254         const char *cmd;
255         char cmdline[LINE_MAX];
256         char myname[_PW_NAME_LEN + 1];
257         struct passwd *pw;
258         struct rule *rule;
259         uid_t uid;
260         uid_t target = 0;
261         gid_t groups[NGROUPS_MAX + 1];
262         int ngroups;
263         int i, ch;
264         int sflag = 0;
265
266         parseconfig("/etc/doas.conf");
267
268         while ((ch = getopt(argc, argv, "su:")) != -1) {
269                 switch (ch) {
270                 case 'u':
271                         if (parseuid(optarg, &target) != 0)
272                                 errx(1, "unknown user");
273                         break;
274                 case 's':
275                         sflag = 1;
276                         break;
277                 default:
278                         usage();
279                         break;
280                 }
281         }
282         argv += optind;
283         argc -= optind;
284
285         if ((!sflag && !argc) || (sflag && argc))
286                 usage();
287
288         uid = getuid();
289         pw = getpwuid(uid);
290         if (!pw)
291                 err(1, "getpwuid failed");
292         if (strlcpy(myname, pw->pw_name, sizeof(myname)) >= sizeof(myname))
293                 errx(1, "pw_name too long");
294         ngroups = getgroups(NGROUPS_MAX, groups);
295         if (ngroups == -1)
296                 err(1, "can't get groups");
297         groups[ngroups++] = getgid();
298
299         if (sflag) {
300                 sh = getenv("SHELL");
301                 if (sh == NULL || *sh == '\0')
302                         shargv[0] = pw->pw_shell;
303                 else
304                         shargv[0] = sh;
305                 argv = shargv;
306                 argc = 1;
307         }
308
309         cmd = argv[0];
310         if (strlcpy(cmdline, argv[0], sizeof(cmdline)) >= sizeof(cmdline))
311                 errx(1, "command line too long");
312         for (i = 1; i < argc; i++) {
313                 if (strlcat(cmdline, " ", sizeof(cmdline)) >= sizeof(cmdline))
314                         errx(1, "command line too long");
315                 if (strlcat(cmdline, argv[i], sizeof(cmdline)) >= sizeof(cmdline))
316                         errx(1, "command line too long");
317         }
318
319         if (!permit(uid, groups, ngroups, &rule, target, cmd)) {
320                 syslog(LOG_AUTHPRIV | LOG_NOTICE,
321                     "failed command for %s: %s", myname, cmdline);
322                 fail();
323         }
324
325         if (!(rule->options & NOPASS)) {
326                 if (!auth_userokay(myname, NULL, NULL, NULL)) {
327                         syslog(LOG_AUTHPRIV | LOG_NOTICE,
328                             "failed password for %s", myname);
329                         fail();
330                 }
331         }
332         envp = copyenv((const char **)envp, rule);
333
334         pw = getpwuid(target);
335         if (!pw)
336                 errx(1, "no passwd entry for target");
337         if (setusercontext(NULL, pw, target, LOGIN_SETGROUP |
338             LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
339             LOGIN_SETUSER) != 0)
340                 errx(1, "failed to set user context for target");
341
342         syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command as %s: %s",
343             myname, pw->pw_name, cmdline);
344         if (setenv("PATH", safepath, 1) == -1)
345                 err(1, "failed to set PATH '%s'", safepath);
346         execvpe(cmd, argv, envp);
347         if (errno == ENOENT)
348                 errx(1, "%s: command not found", cmd);
349         err(1, "%s", cmd);
350 }