xref: /freebsd/sys/security/mac_do/mac_do.c (revision fa4352b74580832d7b501d34d09a564438a82c3d)
1 /*-
2  * Copyright(c) 2024 Baptiste Daroussin <bapt@FreeBSD.org>
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  */
6 
7 #include <sys/param.h>
8 #include <sys/systm.h>
9 #include <sys/jail.h>
10 #include <sys/kernel.h>
11 #include <sys/lock.h>
12 #include <sys/malloc.h>
13 #include <sys/module.h>
14 #include <sys/mount.h>
15 #include <sys/mutex.h>
16 #include <sys/priv.h>
17 #include <sys/proc.h>
18 #include <sys/socket.h>
19 #include <sys/sx.h>
20 #include <sys/sysctl.h>
21 #include <sys/ucred.h>
22 #include <sys/vnode.h>
23 
24 #include <security/mac/mac_policy.h>
25 
26 static SYSCTL_NODE(_security_mac, OID_AUTO, do,
27     CTLFLAG_RW|CTLFLAG_MPSAFE, 0, "mac_do policy controls");
28 
29 static int	do_enabled = 1;
30 SYSCTL_INT(_security_mac_do, OID_AUTO, enabled, CTLFLAG_RWTUN,
31     &do_enabled, 0, "Enforce do policy");
32 
33 static MALLOC_DEFINE(M_DO, "do_rule", "Rules for mac_do");
34 
35 #define MAC_RULE_STRING_LEN	1024
36 
37 static unsigned		mac_do_osd_jail_slot;
38 
39 #define RULE_UID	1
40 #define RULE_GID	2
41 #define RULE_ANY	3
42 
43 struct rule {
44 	int	from_type;
45 	union {
46 		uid_t f_uid;
47 		gid_t f_gid;
48 	};
49 	int	to_type;
50 	uid_t t_uid;
51 	TAILQ_ENTRY(rule) r_entries;
52 };
53 
54 struct rules {
55 	char string[MAC_RULE_STRING_LEN];
56 	TAILQ_HEAD(rulehead, rule) head;
57 };
58 
59 static void
60 toast_rules(struct rules *const rules)
61 {
62 	struct rulehead *const head = &rules->head;
63 	struct rule *rule;
64 
65 	while ((rule = TAILQ_FIRST(head)) != NULL) {
66 		TAILQ_REMOVE(head, rule, r_entries);
67 		free(rule, M_DO);
68 	}
69 	free(rules, M_DO);
70 }
71 
72 static struct rules *
73 alloc_rules(void)
74 {
75 	struct rules *const rules = malloc(sizeof(*rules), M_DO, M_WAITOK);
76 
77 	_Static_assert(MAC_RULE_STRING_LEN > 0, "MAC_RULE_STRING_LEN <= 0!");
78 	rules->string[0] = 0;
79 	TAILQ_INIT(&rules->head);
80 	return (rules);
81 }
82 
83 static int
84 parse_rule_element(char *element, struct rule **rule)
85 {
86 	int error = 0;
87 	char *type, *id, *p;
88 	struct rule *new;
89 
90 	new = malloc(sizeof(*new), M_DO, M_ZERO|M_WAITOK);
91 
92 	type = strsep(&element, "=");
93 	if (type == NULL) {
94 		error = EINVAL;
95 		goto error;
96 	}
97 
98 	if (strcmp(type, "uid") == 0)
99 		new->from_type = RULE_UID;
100 	else if (strcmp(type, "gid") == 0)
101 		new->from_type = RULE_GID;
102 	else {
103 		error = EINVAL;
104 		goto error;
105 	}
106 
107 	id = strsep(&element, ":");
108 	if (id == NULL || *id == '\0') {
109 		error = EINVAL;
110 		goto error;
111 	}
112 
113 	switch (new->from_type) {
114 	case RULE_UID:
115 		new->f_uid = strtol(id, &p, 10);
116 		break;
117 	case RULE_GID:
118 		new->f_gid = strtol(id, &p, 10);
119 		break;
120 	default:
121 		__assert_unreachable();
122 	}
123 	if (*p != '\0') {
124 		error = EINVAL;
125 		goto error;
126 	}
127 
128 	if (element == NULL || *element == '\0') {
129 		error = EINVAL;
130 		goto error;
131 	}
132 	if (strcmp(element, "any") == 0 || strcmp(element, "*") == 0)
133 		new->to_type = RULE_ANY;
134 	else {
135 		new->to_type = RULE_UID;
136 		new->t_uid = strtol(element, &p, 10);
137 		if (*p != '\0') {
138 			error = EINVAL;
139 			goto error;
140 		}
141 	}
142 
143 	MPASS(error == 0);
144 	*rule = new;
145 	return (0);
146 error:
147 	MPASS(error != 0);
148 	free(new, M_DO);
149 	*rule = NULL;
150 	return (error);
151 }
152 
153 /*
154  * Parse rules specification and produce rule structures out of it.
155  *
156  * Returns 0 on success, with '*rulesp' made to point to a 'struct rule'
157  * representing the rules.  On error, the returned value is non-zero and
158  * '*rulesp' is unchanged.  If 'string' has length greater or equal to
159  * MAC_RULE_STRING_LEN, ENAMETOOLONG is returned.  If it is not in the expected
160  * format (comma-separated list of clauses of the form "<type>=<val>:<target>",
161  * where <type> is "uid" or "gid", <val> an UID or GID (depending on <type>) and
162  * <target> is "*", "any" or some UID), EINVAL is returned.
163  */
164 static int
165 parse_rules(const char *const string, struct rules **const rulesp)
166 {
167 	const size_t len = strlen(string);
168 	char *copy;
169 	char *p;
170 	char *element;
171 	struct rules *rules;
172 	struct rule *new;
173 	int error = 0;
174 
175 	if (len >= MAC_RULE_STRING_LEN)
176 		return (ENAMETOOLONG);
177 
178 	rules = alloc_rules();
179 	bcopy(string, rules->string, len + 1);
180 	MPASS(rules->string[len] == '\0'); /* Catch some races. */
181 
182 	copy = malloc(len + 1, M_DO, M_WAITOK);
183 	bcopy(string, copy, len + 1);
184 	MPASS(copy[len] == '\0'); /* Catch some races. */
185 
186 	p = copy;
187 	while ((element = strsep(&p, ",")) != NULL) {
188 		if (element[0] == '\0')
189 			continue;
190 		error = parse_rule_element(element, &new);
191 		if (error != 0) {
192 			toast_rules(rules);
193 			goto out;
194 		}
195 		TAILQ_INSERT_TAIL(&rules->head, new, r_entries);
196 	}
197 
198 	*rulesp = rules;
199 out:
200 	free(copy, M_DO);
201 	return (error);
202 }
203 
204 /*
205  * Find rules applicable to the passed prison.
206  *
207  * Returns the applicable rules (and never NULL).  'pr' must be unlocked.
208  * 'aprp' is set to the (ancestor) prison holding these, and it must be unlocked
209  * once the caller is done accessing the rules.  '*aprp' is equal to 'pr' if and
210  * only if the current jail has its own set of rules.
211  */
212 static struct rules *
213 find_rules(struct prison *const pr, struct prison **const aprp)
214 {
215 	struct prison *cpr, *ppr;
216 	struct rules *rules;
217 
218 	cpr = pr;
219 	for (;;) {
220 		prison_lock(cpr);
221 		rules = osd_jail_get(cpr, mac_do_osd_jail_slot);
222 		if (rules != NULL)
223 			break;
224 		prison_unlock(cpr);
225 
226 		ppr = cpr->pr_parent;
227 		MPASS(ppr != NULL); /* prison0 always has rules. */
228 		cpr = ppr;
229 	}
230 	*aprp = cpr;
231 
232 	return (rules);
233 }
234 
235 /*
236  * OSD destructor for slot 'mac_do_osd_jail_slot'.
237  *
238  * Called with 'value' not NULL.
239  */
240 static void
241 dealloc_osd(void *const value)
242 {
243 	struct rules *const rules = value;
244 
245 	toast_rules(rules);
246 }
247 
248 /*
249  * Remove the rules specifically associated to a prison.
250  *
251  * In practice, this means that the rules become inherited (from the closest
252  * ascendant that has some).
253  *
254  * Destroys the 'mac_do_osd_jail_slot' slot of the passed jail.
255  */
256 static void
257 remove_rules(struct prison *const pr)
258 {
259 	prison_lock(pr);
260 	/* This calls destructor dealloc_osd(). */
261 	osd_jail_del(pr, mac_do_osd_jail_slot);
262 	prison_unlock(pr);
263 }
264 
265 /*
266  * Assign already built rules to a jail.
267  */
268 static void
269 set_rules(struct prison *const pr, struct rules *const rules)
270 {
271 	struct rules *old_rules;
272 	void **rsv;
273 
274 	rsv = osd_reserve(mac_do_osd_jail_slot);
275 
276 	prison_lock(pr);
277 	old_rules = osd_jail_get(pr, mac_do_osd_jail_slot);
278 	osd_jail_set_reserved(pr, mac_do_osd_jail_slot, rsv, rules);
279 	prison_unlock(pr);
280 	if (old_rules != NULL)
281 		toast_rules(old_rules);
282 }
283 
284 /*
285  * Assigns empty rules to a jail.
286  */
287 static void
288 set_empty_rules(struct prison *const pr)
289 {
290 	struct rules *const rules = alloc_rules();
291 
292 	set_rules(pr, rules);
293 }
294 
295 /*
296  * Parse a rules specification and assign them to a jail.
297  *
298  * Returns the same error code as parse_rules() (which see).
299  */
300 static int
301 parse_and_set_rules(struct prison *const pr, const char *rules_string)
302 {
303 	struct rules *rules;
304 	int error;
305 
306 	error = parse_rules(rules_string, &rules);
307 	if (error != 0)
308 		return (error);
309 	set_rules(pr, rules);
310 	return (0);
311 }
312 
313 static int
314 mac_do_sysctl_rules(SYSCTL_HANDLER_ARGS)
315 {
316 	char *const buf = malloc(MAC_RULE_STRING_LEN, M_DO, M_WAITOK);
317 	struct prison *const td_pr = req->td->td_ucred->cr_prison;
318 	struct prison *pr;
319 	struct rules *rules;
320 	int error;
321 
322 	rules = find_rules(td_pr, &pr);
323 	strlcpy(buf, rules->string, MAC_RULE_STRING_LEN);
324 	prison_unlock(pr);
325 
326 	error = sysctl_handle_string(oidp, buf, MAC_RULE_STRING_LEN, req);
327 	if (error != 0 || req->newptr == NULL)
328 		goto out;
329 
330 	/* Set our prison's rules, not that of the jail we inherited from. */
331 	error = parse_and_set_rules(td_pr, buf);
332 out:
333 	free(buf, M_DO);
334 	return (error);
335 }
336 
337 SYSCTL_PROC(_security_mac_do, OID_AUTO, rules,
338     CTLTYPE_STRING|CTLFLAG_RW|CTLFLAG_PRISON|CTLFLAG_MPSAFE,
339     0, 0, mac_do_sysctl_rules, "A",
340     "Rules");
341 
342 
343 SYSCTL_JAIL_PARAM_SYS_SUBNODE(mac, do, CTLFLAG_RW, "Jail MAC/do parameters");
344 SYSCTL_JAIL_PARAM_STRING(_mac_do, rules, CTLFLAG_RW, MAC_RULE_STRING_LEN,
345     "Jail MAC/do rules");
346 
347 
348 static int
349 mac_do_jail_create(void *obj, void *data __unused)
350 {
351 	struct prison *const pr = obj;
352 
353 	set_empty_rules(pr);
354 	return (0);
355 }
356 
357 static int
358 mac_do_jail_get(void *obj, void *data)
359 {
360 	struct prison *ppr, *const pr = obj;
361 	struct vfsoptlist *const opts = data;
362 	struct rules *rules;
363 	int jsys, error;
364 
365 	rules = find_rules(pr, &ppr);
366 
367 	jsys = pr == ppr ?
368 	    (TAILQ_EMPTY(&rules->head) ? JAIL_SYS_DISABLE : JAIL_SYS_NEW) :
369 	    JAIL_SYS_INHERIT;
370 	error = vfs_setopt(opts, "mac.do", &jsys, sizeof(jsys));
371 	if (error != 0 && error != ENOENT)
372 		goto done;
373 
374 	error = vfs_setopts(opts, "mac.do.rules", rules->string);
375 	if (error != 0 && error != ENOENT)
376 		goto done;
377 
378 	error = 0;
379 done:
380 	prison_unlock(ppr);
381 	return (error);
382 }
383 
384 /*
385  * -1 is used as a sentinel in mac_do_jail_check() and mac_do_jail_set() below.
386  */
387 _Static_assert(-1 != JAIL_SYS_DISABLE && -1 != JAIL_SYS_NEW &&
388     -1 != JAIL_SYS_INHERIT,
389     "mac_do(4) uses -1 as a sentinel for uninitialized 'jsys'.");
390 
391 /*
392  * We perform only cheap checks here, i.e., we do not really parse the rules
393  * specification string, if any.
394  */
395 static int
396 mac_do_jail_check(void *obj, void *data)
397 {
398 	struct vfsoptlist *opts = data;
399 	char *rules_string;
400 	int error, jsys, size;
401 
402 	error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys));
403 	if (error == ENOENT)
404 		jsys = -1;
405 	else {
406 		if (error != 0)
407 			return (error);
408 		if (jsys != JAIL_SYS_DISABLE && jsys != JAIL_SYS_NEW &&
409 		    jsys != JAIL_SYS_INHERIT)
410 			return (EINVAL);
411 	}
412 
413 	/*
414 	 * We use vfs_getopt() here instead of vfs_getopts() to get the length.
415 	 * We perform the additional checks done by the latter here, even if
416 	 * jail_set() calls vfs_getopts() itself later (they becoming
417 	 * inconsistent wouldn't cause any security problem).
418 	 */
419 	error = vfs_getopt(opts, "mac.do.rules", (void**)&rules_string, &size);
420 	if (error == ENOENT) {
421 		/*
422 		 * Default (in absence of "mac.do.rules") is to disable (and, in
423 		 * particular, not inherit).
424 		 */
425 		if (jsys == -1)
426 			jsys = JAIL_SYS_DISABLE;
427 
428 		if (jsys == JAIL_SYS_NEW) {
429 			vfs_opterror(opts, "'mac.do.rules' must be specified "
430 			    "given 'mac.do''s value");
431 			return (EINVAL);
432 		}
433 
434 		/* Absence of "mac.do.rules" at this point is OK. */
435 		error = 0;
436 	} else {
437 		if (error != 0)
438 			return (error);
439 
440 		/* Not a proper string. */
441 		if (size == 0 || rules_string[size - 1] != '\0') {
442 			vfs_opterror(opts, "'mac.do.rules' not a proper string");
443 			return (EINVAL);
444 		}
445 
446 		if (size > MAC_RULE_STRING_LEN) {
447 			vfs_opterror(opts, "'mdo.rules' too long");
448 			return (ENAMETOOLONG);
449 		}
450 
451 		if (jsys == -1)
452 			/* Default (if "mac.do.rules" is present). */
453 			jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE :
454 			    JAIL_SYS_NEW;
455 
456 		/*
457 		 * Be liberal and accept JAIL_SYS_DISABLE and JAIL_SYS_INHERIT
458 		 * with an explicit empty rules specification.
459 		 */
460 		switch (jsys) {
461 		case JAIL_SYS_DISABLE:
462 		case JAIL_SYS_INHERIT:
463 			if (rules_string[0] != '\0') {
464 				vfs_opterror(opts, "'mac.do.rules' specified "
465 				    "but should not given 'mac.do''s value");
466 				return (EINVAL);
467 			}
468 			break;
469 		}
470 	}
471 
472 	return (error);
473 }
474 
475 static int
476 mac_do_jail_set(void *obj, void *data)
477 {
478 	struct prison *pr = obj;
479 	struct vfsoptlist *opts = data;
480 	char *rules_string;
481 	int error, jsys;
482 
483 	/*
484 	 * The invariants checks used below correspond to what has already been
485 	 * checked in jail_check() above.
486 	 */
487 
488 	error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys));
489 	MPASS(error == 0 || error == ENOENT);
490 	if (error != 0)
491 		jsys = -1; /* Mark unfilled. */
492 
493 	rules_string = vfs_getopts(opts, "mac.do.rules", &error);
494 	MPASS(error == 0 || error == ENOENT);
495 	if (error == 0) {
496 		MPASS(strlen(rules_string) < MAC_RULE_STRING_LEN);
497 		if (jsys == -1)
498 			/* Default (if "mac.do.rules" is present). */
499 			jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE :
500 			    JAIL_SYS_NEW;
501 		else
502 			MPASS(jsys == JAIL_SYS_NEW ||
503 			    ((jsys == JAIL_SYS_DISABLE ||
504 			    jsys == JAIL_SYS_INHERIT) &&
505 			    rules_string[0] == '\0'));
506 	} else {
507 		MPASS(jsys != JAIL_SYS_NEW);
508 		if (jsys == -1)
509 			/*
510 			 * Default (in absence of "mac.do.rules") is to disable
511 			 * (and, in particular, not inherit).
512 			 */
513 			jsys = JAIL_SYS_DISABLE;
514 		/* If disabled, we'll store an empty rule specification. */
515 		if (jsys == JAIL_SYS_DISABLE)
516 			rules_string = "";
517 	}
518 
519 	switch (jsys) {
520 	case JAIL_SYS_INHERIT:
521 		remove_rules(pr);
522 		error = 0;
523 		break;
524 	case JAIL_SYS_DISABLE:
525 	case JAIL_SYS_NEW:
526 		error = parse_and_set_rules(pr, rules_string);
527 		break;
528 	default:
529 		__assert_unreachable();
530 	}
531 	return (error);
532 }
533 
534 /*
535  * OSD jail methods.
536  *
537  * There is no PR_METHOD_REMOVE, as OSD storage is destroyed by the common jail
538  * code (see prison_cleanup()), which triggers a run of our dealloc_osd()
539  * destructor.
540  */
541 static const osd_method_t osd_methods[PR_MAXMETHOD] = {
542 	[PR_METHOD_CREATE] = mac_do_jail_create,
543 	[PR_METHOD_GET] = mac_do_jail_get,
544 	[PR_METHOD_CHECK] = mac_do_jail_check,
545 	[PR_METHOD_SET] = mac_do_jail_set,
546 };
547 
548 
549 static void
550 mac_do_init(struct mac_policy_conf *mpc)
551 {
552 	struct prison *pr;
553 
554 	mac_do_osd_jail_slot = osd_jail_register(dealloc_osd, osd_methods);
555 	set_empty_rules(&prison0);
556 	sx_slock(&allprison_lock);
557 	TAILQ_FOREACH(pr, &allprison, pr_list)
558 	    set_empty_rules(pr);
559 	sx_sunlock(&allprison_lock);
560 }
561 
562 static void
563 mac_do_destroy(struct mac_policy_conf *mpc)
564 {
565 	osd_jail_deregister(mac_do_osd_jail_slot);
566 }
567 
568 static bool
569 rule_applies(struct ucred *cred, struct rule *r)
570 {
571 	if (r->from_type == RULE_UID && r->f_uid == cred->cr_uid)
572 		return (true);
573 	if (r->from_type == RULE_GID && groupmember(r->f_gid, cred))
574 		return (true);
575 	return (false);
576 }
577 
578 static int
579 mac_do_priv_grant(struct ucred *cred, int priv)
580 {
581 	struct rule *r;
582 	struct prison *pr;
583 	struct rules *rule;
584 
585 	if (do_enabled == 0)
586 		return (EPERM);
587 
588 	rule = find_rules(cred->cr_prison, &pr);
589 	TAILQ_FOREACH(r, &rule->head, r_entries) {
590 		if (rule_applies(cred, r)) {
591 			switch (priv) {
592 			case PRIV_CRED_SETGROUPS:
593 			case PRIV_CRED_SETUID:
594 				prison_unlock(pr);
595 				return (0);
596 			default:
597 				break;
598 			}
599 		}
600 	}
601 	prison_unlock(pr);
602 	return (EPERM);
603 }
604 
605 static int
606 mac_do_check_setgroups(struct ucred *cred, int ngrp, gid_t *groups)
607 {
608 	struct rule *r;
609 	char *fullpath = NULL;
610 	char *freebuf = NULL;
611 	struct prison *pr;
612 	struct rules *rule;
613 
614 	if (do_enabled == 0)
615 		return (0);
616 	if (cred->cr_uid == 0)
617 		return (0);
618 
619 	if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0)
620 		return (EPERM);
621 	if (strcmp(fullpath, "/usr/bin/mdo") != 0) {
622 		free(freebuf, M_TEMP);
623 		return (EPERM);
624 	}
625 	free(freebuf, M_TEMP);
626 
627 	rule = find_rules(cred->cr_prison, &pr);
628 	TAILQ_FOREACH(r, &rule->head, r_entries) {
629 		if (rule_applies(cred, r)) {
630 			prison_unlock(pr);
631 			return (0);
632 		}
633 	}
634 	prison_unlock(pr);
635 
636 	return (EPERM);
637 }
638 
639 static int
640 mac_do_check_setuid(struct ucred *cred, uid_t uid)
641 {
642 	struct rule *r;
643 	int error;
644 	char *fullpath = NULL;
645 	char *freebuf = NULL;
646 	struct prison *pr;
647 	struct rules *rule;
648 
649 	if (do_enabled == 0)
650 		return (0);
651 	if (cred->cr_uid == uid || cred->cr_uid == 0 || cred->cr_ruid == 0)
652 		return (0);
653 
654 	if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0)
655 		return (EPERM);
656 	if (strcmp(fullpath, "/usr/bin/mdo") != 0) {
657 		free(freebuf, M_TEMP);
658 		return (EPERM);
659 	}
660 	free(freebuf, M_TEMP);
661 
662 	error = EPERM;
663 	rule = find_rules(cred->cr_prison, &pr);
664 	TAILQ_FOREACH(r, &rule->head, r_entries) {
665 		if (r->from_type == RULE_UID) {
666 			if (cred->cr_uid != r->f_uid)
667 				continue;
668 			if (r->to_type == RULE_ANY) {
669 				error = 0;
670 				break;
671 			}
672 			if (r->to_type == RULE_UID && uid == r->t_uid) {
673 				error = 0;
674 				break;
675 			}
676 		}
677 		if (r->from_type == RULE_GID) {
678 			if (!groupmember(r->f_gid, cred))
679 				continue;
680 			if (r->to_type == RULE_ANY) {
681 				error = 0;
682 				break;
683 			}
684 			if (r->to_type == RULE_UID && uid == r->t_uid) {
685 				error = 0;
686 				break;
687 			}
688 		}
689 	}
690 	prison_unlock(pr);
691 	return (error);
692 }
693 
694 static struct mac_policy_ops do_ops = {
695 	.mpo_destroy = mac_do_destroy,
696 	.mpo_init = mac_do_init,
697 	.mpo_cred_check_setuid = mac_do_check_setuid,
698 	.mpo_cred_check_setgroups = mac_do_check_setgroups,
699 	.mpo_priv_grant = mac_do_priv_grant,
700 };
701 
702 MAC_POLICY_SET(&do_ops, mac_do, "MAC/do",
703    MPC_LOADTIME_FLAG_UNLOADOK, NULL);
704 MODULE_VERSION(mac_do, 1);
705