xref: /illumos-gate/usr/src/cmd/pfexecd/pfexecd.c (revision aff0053c6dd5ad7336ddf9693b62e86e8e6be8bf)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  *
21  * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
22  * Copyright 2015, Joyent, Inc.
23  * Copyright 2026 Oxide Computer Company
24  */
25 
26 #define	_POSIX_PTHREAD_SEMANTICS 1
27 
28 #include <sys/ccompile.h>
29 #include <sys/param.h>
30 #include <sys/klpd.h>
31 #include <sys/syscall.h>
32 #include <sys/systeminfo.h>
33 
34 #include <alloca.h>
35 #include <ctype.h>
36 #include <deflt.h>
37 #include <door.h>
38 #include <err.h>
39 #include <errno.h>
40 #include <grp.h>
41 #include <priv.h>
42 #include <pthread.h>
43 #include <pwd.h>
44 #include <regex.h>
45 #include <secdb.h>
46 #include <signal.h>
47 #include <stdio.h>
48 #include <stdlib.h>
49 #include <string.h>
50 #include <syslog.h>
51 #include <unistd.h>
52 
53 #include <auth_attr.h>
54 #include <exec_attr.h>
55 #include <prof_attr.h>
56 #include <user_attr.h>
57 
58 static int doorfd = -1;
59 
60 static size_t repsz, setsz;
61 
62 static uid_t get_uid(const char *, boolean_t *, char *);
63 static gid_t get_gid(const char *, boolean_t *, char *);
64 static priv_set_t *get_privset(const char *, boolean_t *, char *);
65 static priv_set_t *get_granted_privs(uid_t);
66 
67 /*
68  * Remove the isaexec path of an executable if we can't find the
69  * executable at the first attempt.
70  */
71 
72 static regex_t regc;
73 static boolean_t cansplice = B_TRUE;
74 
75 static void
76 init_isa_regex(void)
77 {
78 	char *isalist;
79 	size_t isalen = 255;		/* wild guess */
80 	size_t len;
81 	long ret;
82 	char *regexpr;
83 	char *p;
84 
85 	/*
86 	 * Extract the isalist(7) for userland from the kernel.
87 	 */
88 	isalist = malloc(isalen);
89 	do {
90 		ret = sysinfo(SI_ISALIST, isalist, isalen);
91 		if (ret == -1l) {
92 			free(isalist);
93 			return;
94 		}
95 		if (ret > isalen) {
96 			isalen = ret;
97 			isalist = realloc(isalist, isalen);
98 		} else
99 			break;
100 	} while (isalist != NULL);
101 
102 
103 	if (isalist == NULL)
104 		return;
105 
106 	/* allocate room for the regex + (/())/[^/]*$ + needed \\. */
107 #define	LEFT	"(/("
108 #define	RIGHT	"))/[^/]*$"
109 
110 	regexpr = alloca(ret * 2 + sizeof (LEFT RIGHT));
111 	(void) strcpy(regexpr, LEFT);
112 	len = strlen(regexpr);
113 
114 	for (p = isalist; *p; p++) {
115 		switch (*p) {
116 		case '+':
117 		case '|':
118 		case '*':
119 		case '[':
120 		case ']':
121 		case '{':
122 		case '}':
123 		case '\\':
124 			regexpr[len++] = '\\';
125 			/* FALLTHROUGH */
126 		default:
127 			regexpr[len++] = *p;
128 			break;
129 		case ' ':
130 		case '\t':
131 			regexpr[len++] = '|';
132 			break;
133 		}
134 	}
135 
136 	free(isalist);
137 	regexpr[len] = '\0';
138 	(void) strcat(regexpr, RIGHT);
139 
140 	if (regcomp(&regc, regexpr, REG_EXTENDED) != 0)
141 		return;
142 
143 	cansplice = B_TRUE;
144 }
145 
146 #define	NMATCH	2
147 
148 static boolean_t
149 removeisapath(char *path)
150 {
151 	regmatch_t match[NMATCH];
152 
153 	if (!cansplice || regexec(&regc, path, NMATCH, match, 0) != 0)
154 		return (B_FALSE);
155 
156 	/*
157 	 * The first match includes the whole matched expression including the
158 	 * end of the string.  The second match includes the "/" + "isa" and
159 	 * that is the part we need to remove.
160 	 */
161 
162 	if (match[1].rm_so == -1)
163 		return (B_FALSE);
164 
165 	/* match[0].rm_eo == strlen(path) */
166 	(void) memmove(path + match[1].rm_so, path + match[1].rm_eo,
167 	    match[0].rm_eo - match[1].rm_eo + 1);
168 
169 	return (B_TRUE);
170 }
171 
172 static int
173 register_pfexec(int fd)
174 {
175 	int ret = syscall(SYS_privsys, PRIVSYS_PFEXEC_REG, fd);
176 
177 	return (ret);
178 }
179 
180 static void
181 unregister_pfexec(int sig __unused)
182 {
183 	if (doorfd != -1)
184 		(void) syscall(SYS_privsys, PRIVSYS_PFEXEC_UNREG, doorfd);
185 	_exit(0);
186 }
187 
188 static int
189 alldigits(const char *s)
190 {
191 	int c;
192 
193 	if (*s == '\0')
194 		return (0);
195 
196 	while ((c = *s++) != '\0') {
197 		if (!isdigit(c)) {
198 			return (0);
199 		}
200 	}
201 
202 	return (1);
203 }
204 
205 static uid_t
206 get_uid(const char *v, boolean_t *ok, char *path)
207 {
208 	struct passwd *pwd, pwdm;
209 	char buf[1024];
210 
211 	if (getpwnam_r(v, &pwdm, buf, sizeof (buf), &pwd) == 0 && pwd != NULL)
212 		return (pwd->pw_uid);
213 
214 	if (alldigits(v))
215 		return (atoi(v));
216 
217 	*ok = B_FALSE;
218 	syslog(LOG_ERR, "%s: %s: unknown username\n", path, v);
219 	return ((uid_t)-1);
220 }
221 
222 static uid_t
223 get_gid(const char *v, boolean_t *ok, char *path)
224 {
225 	struct group *grp, grpm;
226 	char buf[1024];
227 
228 	if (getgrnam_r(v, &grpm, buf, sizeof (buf), &grp) == 0 && grp != NULL)
229 		return (grp->gr_gid);
230 
231 	if (alldigits(v))
232 		return (atoi(v));
233 
234 	*ok = B_FALSE;
235 	syslog(LOG_ERR, "%s: %s: unknown groupname\n", path, v);
236 	return ((gid_t)-1);
237 }
238 
239 static priv_set_t *
240 get_privset(const char *s, boolean_t *ok, char *path)
241 {
242 	priv_set_t *res;
243 
244 	if ((res = priv_str_to_set(s, ",", NULL)) == NULL) {
245 		syslog(LOG_ERR, "%s: %s: bad privilege set\n", path, s);
246 		if (ok != NULL)
247 			*ok = B_FALSE;
248 	}
249 	return (res);
250 }
251 
252 static int
253 ggp_callback(const char *prof __unused, kva_t *attr, void *ctxt __unused,
254     void *vres)
255 {
256 	priv_set_t *res = vres;
257 	char *privs;
258 
259 	if (attr == NULL)
260 		return (0);
261 
262 	/* get privs from this profile */
263 	privs = kva_match(attr, PROFATTR_PRIVS_KW);
264 	if (privs != NULL) {
265 		priv_set_t *tmp = priv_str_to_set(privs, ",", NULL);
266 		if (tmp != NULL) {
267 			priv_union(tmp, res);
268 			priv_freeset(tmp);
269 		}
270 	}
271 
272 	return (0);
273 }
274 
275 /*
276  * This routine exists on failure and returns NULL if no granted privileges
277  * are set.
278  */
279 static priv_set_t *
280 get_granted_privs(uid_t uid)
281 {
282 	priv_set_t *res;
283 	struct passwd *pwd, pwdm;
284 	char buf[1024];
285 
286 	if (getpwuid_r(uid, &pwdm, buf, sizeof (buf), &pwd) != 0 || pwd == NULL)
287 		return (NULL);
288 
289 	res = priv_allocset();
290 	if (res == NULL)
291 		return (NULL);
292 
293 	priv_emptyset(res);
294 
295 	(void) _enum_profs(pwd->pw_name, ggp_callback, NULL, res);
296 
297 	return (res);
298 }
299 
300 static void
301 callback_forced_privs(pfexec_arg_t *pap)
302 {
303 	execattr_t *exec;
304 	char *value;
305 	priv_set_t *fset;
306 	void *res = alloca(setsz);
307 
308 	/* Empty set signifies no forced privileges. */
309 	priv_emptyset(res);
310 
311 	exec = getexecprof("Forced Privilege", KV_COMMAND, pap->pfa_path,
312 	    GET_ONE);
313 
314 	if (exec == NULL && removeisapath(pap->pfa_path)) {
315 		exec = getexecprof("Forced Privilege", KV_COMMAND,
316 		    pap->pfa_path, GET_ONE);
317 	}
318 
319 	if (exec == NULL) {
320 		(void) door_return(res, setsz, NULL, 0);
321 		return;
322 	}
323 
324 	if ((value = kva_match(exec->attr, EXECATTR_IPRIV_KW)) == NULL ||
325 	    (fset = get_privset(value, NULL, pap->pfa_path)) == NULL) {
326 		free_execattr(exec);
327 		(void) door_return(res, setsz, NULL, 0);
328 		return;
329 	}
330 
331 	priv_copyset(fset, res);
332 	priv_freeset(fset);
333 
334 	free_execattr(exec);
335 	(void) door_return(res, setsz, NULL, 0);
336 }
337 
338 static void
339 callback_user_privs(pfexec_arg_t *pap)
340 {
341 	priv_set_t *gset, *wset;
342 	uint32_t res;
343 
344 	wset = (priv_set_t *)&pap->pfa_buf;
345 	gset = get_granted_privs(pap->pfa_uid);
346 
347 	res = priv_issubset(wset, gset);
348 	priv_freeset(gset);
349 
350 	(void) door_return((char *)&res, sizeof (res), NULL, 0);
351 }
352 
353 static void
354 callback_pfexec(pfexec_arg_t *pap)
355 {
356 	pfexec_reply_t *res = alloca(repsz);
357 	uid_t uid, euid, uuid;
358 	gid_t gid, egid;
359 	struct passwd pw, *pwd;
360 	char buf[1024];
361 	execattr_t *exec = NULL;
362 	char *value;
363 	priv_set_t *lset, *iset;
364 	size_t mysz = repsz - 2 * setsz;
365 	char *path = pap->pfa_path;
366 
367 	/*
368 	 * Initialize the pfexec_reply_t to a sane state.
369 	 */
370 	res->pfr_vers = pap->pfa_vers;
371 	res->pfr_len = 0;
372 	res->pfr_ruid = PFEXEC_NOTSET;
373 	res->pfr_euid = PFEXEC_NOTSET;
374 	res->pfr_rgid = PFEXEC_NOTSET;
375 	res->pfr_egid = PFEXEC_NOTSET;
376 	res->pfr_setcred = B_FALSE;
377 	res->pfr_scrubenv = B_TRUE;
378 	res->pfr_allowed = B_FALSE;
379 	res->pfr_ioff = 0;
380 	res->pfr_loff = 0;
381 
382 	uuid = pap->pfa_uid;
383 
384 	if (getpwuid_r(uuid, &pw, buf, sizeof (buf), &pwd) != 0 || pwd == NULL)
385 		goto stdexec;
386 
387 	exec = getexecuser(pwd->pw_name, KV_COMMAND, path, GET_ONE);
388 
389 	if ((exec == NULL || exec->attr == NULL) && removeisapath(path)) {
390 		free_execattr(exec);
391 		exec = getexecuser(pwd->pw_name, KV_COMMAND, path, GET_ONE);
392 	}
393 
394 	if (exec == NULL) {
395 		res->pfr_allowed = B_FALSE;
396 		goto ret;
397 	}
398 
399 	if (exec->attr == NULL)
400 		goto stdexec;
401 
402 	/* Found in execattr, so clearly we can use it */
403 	res->pfr_allowed = B_TRUE;
404 
405 	uid = euid = (uid_t)-1;
406 	gid = egid = (gid_t)-1;
407 	lset = iset = NULL;
408 
409 	/*
410 	 * If there's an error in parsing uid, gid, privs, then return
411 	 * failure.
412 	 */
413 	if ((value = kva_match(exec->attr, EXECATTR_UID_KW)) != NULL)
414 		euid = uid = get_uid(value, &res->pfr_allowed, path);
415 
416 	if ((value = kva_match(exec->attr, EXECATTR_GID_KW)) != NULL)
417 		egid = gid = get_gid(value, &res->pfr_allowed, path);
418 
419 	if ((value = kva_match(exec->attr, EXECATTR_EUID_KW)) != NULL)
420 		euid = get_uid(value, &res->pfr_allowed, path);
421 
422 	if ((value = kva_match(exec->attr, EXECATTR_EGID_KW)) != NULL)
423 		egid = get_gid(value, &res->pfr_allowed, path);
424 
425 	if ((value = kva_match(exec->attr, EXECATTR_LPRIV_KW)) != NULL)
426 		lset = get_privset(value, &res->pfr_allowed, path);
427 
428 	if ((value = kva_match(exec->attr, EXECATTR_IPRIV_KW)) != NULL)
429 		iset = get_privset(value, &res->pfr_allowed, path);
430 
431 	/*
432 	 * Remove LD_* variables in the kernel when the runtime linker might
433 	 * use them later on because the uids are equal.
434 	 */
435 	res->pfr_scrubenv = (uid != (uid_t)-1 && euid == uid) ||
436 	    (gid != (gid_t)-1 && egid == gid) || iset != NULL;
437 
438 	res->pfr_euid = euid;
439 	res->pfr_ruid = uid;
440 	res->pfr_egid = egid;
441 	res->pfr_rgid = gid;
442 
443 	/* Now add the privilege sets */
444 	res->pfr_ioff = res->pfr_loff = 0;
445 	if (iset != NULL) {
446 		res->pfr_ioff = mysz;
447 		priv_copyset(iset, PFEXEC_REPLY_IPRIV(res));
448 		mysz += setsz;
449 		priv_freeset(iset);
450 	}
451 	if (lset != NULL) {
452 		res->pfr_loff = mysz;
453 		priv_copyset(lset, PFEXEC_REPLY_LPRIV(res));
454 		mysz += setsz;
455 		priv_freeset(lset);
456 	}
457 
458 	res->pfr_setcred = uid != (uid_t)-1 || euid != (uid_t)-1 ||
459 	    egid != (gid_t)-1 || gid != (gid_t)-1 || iset != NULL ||
460 	    lset != NULL;
461 
462 	/* If the real uid changes, we stop running under a profile shell */
463 	res->pfr_clearflag = uid != (uid_t)-1 && uid != uuid;
464 	free_execattr(exec);
465 ret:
466 	(void) door_return((char *)res, mysz, NULL, 0);
467 	return;
468 
469 stdexec:
470 	free_execattr(exec);
471 
472 	res->pfr_scrubenv = B_FALSE;
473 	res->pfr_setcred = B_FALSE;
474 	res->pfr_allowed = B_TRUE;
475 
476 	(void) door_return((char *)res, mysz, NULL, 0);
477 }
478 
479 static void
480 callback(void *cookie __unused, char *argp, size_t asz,
481     door_desc_t *dp __unused, uint_t ndesc __unused)
482 {
483 	pfexec_arg_t *pap = (pfexec_arg_t *)argp;
484 
485 	if (asz < sizeof (pfexec_arg_t) || pap->pfa_vers != PFEXEC_ARG_VERS) {
486 		(void) door_return(NULL, 0, NULL, 0);
487 		return;
488 	}
489 
490 	switch (pap->pfa_call) {
491 	case PFEXEC_EXEC_ATTRS:
492 		callback_pfexec(pap);
493 		break;
494 	case PFEXEC_FORCED_PRIVS:
495 		callback_forced_privs(pap);
496 		break;
497 	case PFEXEC_USER_PRIVS:
498 		callback_user_privs(pap);
499 		break;
500 	default:
501 		syslog(LOG_ERR, "Bad Call: %d\n", pap->pfa_call);
502 		break;
503 	}
504 
505 	/*
506 	 * If the door_return(ptr, size, NULL, 0) fails, make sure we
507 	 * don't lose server threads.
508 	 */
509 	(void) door_return(NULL, 0, NULL, 0);
510 }
511 
512 /*
513  * Door server threads. The default door server thread creation function in
514  * libc places no limit on the number of threads it will create and gives
515  * each one a default sized (1 MiB) stack. Instead, create a private door
516  * with a fixed pool of server threads that have smaller stacks. The door
517  * is created with DOOR_NO_DEPLETION_CB so the pool never grows beyond its
518  * initial size; the kernel queues further door invocations until a server
519  * thread becomes available.
520  *
521  * The stack size must accommodate libsecdb's profile enumeration, which
522  * keeps MAXPROFS-sized pointer arrays and copies of profile lists on
523  * the stack, with headroom for nested profiles.
524  */
525 #define	DOOR_THREADS		16
526 #define	DOOR_THREAD_STACKSIZE	(256 * 1024)
527 
528 /* Initialised in main() before the door is created, then never modified. */
529 static pthread_attr_t door_attr;
530 
531 /*
532  * Since the door uses DOOR_NO_DEPLETION_CB, this is only called from
533  * door_xcreate() to populate the initial thread pool.
534  */
535 static int
536 create_door_thread(door_info_t *dip __unused, void *(*startf)(void *),
537     void *startfarg, void *cookie __unused)
538 {
539 	if (pthread_create(NULL, &door_attr, startf, startfarg) != 0)
540 		return (-1);
541 	return (1);
542 }
543 
544 int
545 main(void)
546 {
547 	const priv_impl_info_t *info;
548 	int ret;
549 
550 	(void) signal(SIGINT, unregister_pfexec);
551 	(void) signal(SIGQUIT, unregister_pfexec);
552 	(void) signal(SIGTERM, unregister_pfexec);
553 	(void) signal(SIGHUP, unregister_pfexec);
554 
555 	info = getprivimplinfo();
556 	if (info == NULL)
557 		exit(1);
558 
559 	if (fork() > 0)
560 		_exit(0);
561 
562 	openlog("pfexecd", LOG_PID, LOG_DAEMON);
563 	setsz = info->priv_setsize * sizeof (priv_chunk_t);
564 	repsz = 2 * setsz + sizeof (pfexec_reply_t);
565 
566 	init_isa_regex();
567 
568 	if ((ret = pthread_attr_init(&door_attr)) != 0 ||
569 	    (ret = pthread_attr_setdetachstate(&door_attr,
570 	    PTHREAD_CREATE_DETACHED)) != 0 ||
571 	    (ret = pthread_attr_setstacksize(&door_attr,
572 	    DOOR_THREAD_STACKSIZE)) != 0) {
573 		errc(EXIT_FAILURE, ret,
574 		    "failed to configure door thread attributes");
575 	}
576 
577 	doorfd = door_xcreate(callback, NULL,
578 	    DOOR_REFUSE_DESC | DOOR_NO_DEPLETION_CB, create_door_thread,
579 	    NULL, NULL, DOOR_THREADS);
580 
581 	if (doorfd == -1 || register_pfexec(doorfd) != 0) {
582 		perror("doorfd");
583 		exit(1);
584 	}
585 
586 	while (1)
587 		(void) sigpause(SIGINT);
588 
589 	return (0);
590 }
591