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