xref: /openbmc/linux/security/safesetid/lsm.c (revision 2612e3bbc0386368a850140a6c9b990cd496a5ec)
1aeca4e2cSMicah Morton // SPDX-License-Identifier: GPL-2.0
2aeca4e2cSMicah Morton /*
3aeca4e2cSMicah Morton  * SafeSetID Linux Security Module
4aeca4e2cSMicah Morton  *
5aeca4e2cSMicah Morton  * Author: Micah Morton <mortonm@chromium.org>
6aeca4e2cSMicah Morton  *
7aeca4e2cSMicah Morton  * Copyright (C) 2018 The Chromium OS Authors.
8aeca4e2cSMicah Morton  *
9aeca4e2cSMicah Morton  * This program is free software; you can redistribute it and/or modify
10aeca4e2cSMicah Morton  * it under the terms of the GNU General Public License version 2, as
11aeca4e2cSMicah Morton  * published by the Free Software Foundation.
12aeca4e2cSMicah Morton  *
13aeca4e2cSMicah Morton  */
14aeca4e2cSMicah Morton 
15aeca4e2cSMicah Morton #define pr_fmt(fmt) "SafeSetID: " fmt
16aeca4e2cSMicah Morton 
17aeca4e2cSMicah Morton #include <linux/lsm_hooks.h>
18aeca4e2cSMicah Morton #include <linux/module.h>
19aeca4e2cSMicah Morton #include <linux/ptrace.h>
20aeca4e2cSMicah Morton #include <linux/sched/task_stack.h>
21aeca4e2cSMicah Morton #include <linux/security.h>
221cd02a27SJann Horn #include "lsm.h"
23aeca4e2cSMicah Morton 
24aeca4e2cSMicah Morton /* Flag indicating whether initialization completed */
251b8b7192SAustin Kim int safesetid_initialized __initdata;
26aeca4e2cSMicah Morton 
275294bac9SThomas Cedeno struct setid_ruleset __rcu *safesetid_setuid_rules;
285294bac9SThomas Cedeno struct setid_ruleset __rcu *safesetid_setgid_rules;
295294bac9SThomas Cedeno 
30aeca4e2cSMicah Morton 
3103638e62SJann Horn /* Compute a decision for a transition from @src to @dst under @policy. */
_setid_policy_lookup(struct setid_ruleset * policy,kid_t src,kid_t dst)325294bac9SThomas Cedeno enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy,
335294bac9SThomas Cedeno 		kid_t src, kid_t dst)
34aeca4e2cSMicah Morton {
355294bac9SThomas Cedeno 	struct setid_rule *rule;
361cd02a27SJann Horn 	enum sid_policy_type result = SIDPOL_DEFAULT;
37aeca4e2cSMicah Morton 
385294bac9SThomas Cedeno 	if (policy->type == UID) {
395294bac9SThomas Cedeno 		hash_for_each_possible(policy->rules, rule, next, __kuid_val(src.uid)) {
405294bac9SThomas Cedeno 			if (!uid_eq(rule->src_id.uid, src.uid))
411cd02a27SJann Horn 				continue;
425294bac9SThomas Cedeno 			if (uid_eq(rule->dst_id.uid, dst.uid))
431cd02a27SJann Horn 				return SIDPOL_ALLOWED;
441cd02a27SJann Horn 			result = SIDPOL_CONSTRAINED;
45aeca4e2cSMicah Morton 		}
465294bac9SThomas Cedeno 	} else if (policy->type == GID) {
475294bac9SThomas Cedeno 		hash_for_each_possible(policy->rules, rule, next, __kgid_val(src.gid)) {
485294bac9SThomas Cedeno 			if (!gid_eq(rule->src_id.gid, src.gid))
495294bac9SThomas Cedeno 				continue;
505294bac9SThomas Cedeno 			if (gid_eq(rule->dst_id.gid, dst.gid)){
515294bac9SThomas Cedeno 				return SIDPOL_ALLOWED;
525294bac9SThomas Cedeno 			}
535294bac9SThomas Cedeno 			result = SIDPOL_CONSTRAINED;
545294bac9SThomas Cedeno 		}
555294bac9SThomas Cedeno 	} else {
565294bac9SThomas Cedeno 		/* Should not reach here, report the ID as contrainsted */
575294bac9SThomas Cedeno 		result = SIDPOL_CONSTRAINED;
585294bac9SThomas Cedeno 	}
5903638e62SJann Horn 	return result;
6003638e62SJann Horn }
6103638e62SJann Horn 
6203638e62SJann Horn /*
6303638e62SJann Horn  * Compute a decision for a transition from @src to @dst under the active
6403638e62SJann Horn  * policy.
6503638e62SJann Horn  */
setid_policy_lookup(kid_t src,kid_t dst,enum setid_type new_type)665294bac9SThomas Cedeno static enum sid_policy_type setid_policy_lookup(kid_t src, kid_t dst, enum setid_type new_type)
6703638e62SJann Horn {
6803638e62SJann Horn 	enum sid_policy_type result = SIDPOL_DEFAULT;
695294bac9SThomas Cedeno 	struct setid_ruleset *pol;
7003638e62SJann Horn 
7103638e62SJann Horn 	rcu_read_lock();
725294bac9SThomas Cedeno 	if (new_type == UID)
7303638e62SJann Horn 		pol = rcu_dereference(safesetid_setuid_rules);
745294bac9SThomas Cedeno 	else if (new_type == GID)
755294bac9SThomas Cedeno 		pol = rcu_dereference(safesetid_setgid_rules);
765294bac9SThomas Cedeno 	else { /* Should not reach here */
775294bac9SThomas Cedeno 		result = SIDPOL_CONSTRAINED;
785294bac9SThomas Cedeno 		rcu_read_unlock();
795294bac9SThomas Cedeno 		return result;
805294bac9SThomas Cedeno 	}
815294bac9SThomas Cedeno 
825294bac9SThomas Cedeno 	if (pol) {
835294bac9SThomas Cedeno 		pol->type = new_type;
845294bac9SThomas Cedeno 		result = _setid_policy_lookup(pol, src, dst);
855294bac9SThomas Cedeno 	}
86aeca4e2cSMicah Morton 	rcu_read_unlock();
871cd02a27SJann Horn 	return result;
88aeca4e2cSMicah Morton }
89aeca4e2cSMicah Morton 
safesetid_security_capable(const struct cred * cred,struct user_namespace * ns,int cap,unsigned int opts)90aeca4e2cSMicah Morton static int safesetid_security_capable(const struct cred *cred,
91aeca4e2cSMicah Morton 				      struct user_namespace *ns,
92aeca4e2cSMicah Morton 				      int cap,
93aeca4e2cSMicah Morton 				      unsigned int opts)
94aeca4e2cSMicah Morton {
955294bac9SThomas Cedeno 	/* We're only interested in CAP_SETUID and CAP_SETGID. */
965294bac9SThomas Cedeno 	if (cap != CAP_SETUID && cap != CAP_SETGID)
978068866cSJann Horn 		return 0;
988068866cSJann Horn 
99aeca4e2cSMicah Morton 	/*
1003e3374d3SMicah Morton 	 * If CAP_SET{U/G}ID is currently used for a setid or setgroups syscall, we
1013e3374d3SMicah Morton 	 * want to let it go through here; the real security check happens later, in
1023e3374d3SMicah Morton 	 * the task_fix_set{u/g}id or task_fix_setgroups hooks.
1038068866cSJann Horn 	 */
1048068866cSJann Horn 	if ((opts & CAP_OPT_INSETID) != 0)
1058068866cSJann Horn 		return 0;
1068068866cSJann Horn 
1075294bac9SThomas Cedeno 	switch (cap) {
1085294bac9SThomas Cedeno 	case CAP_SETUID:
1098068866cSJann Horn 		/*
1108068866cSJann Horn 		* If no policy applies to this task, allow the use of CAP_SETUID for
1118068866cSJann Horn 		* other purposes.
1128068866cSJann Horn 		*/
11303ca0ec1SThomas Cedeno 		if (setid_policy_lookup((kid_t){.uid = cred->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT)
1148068866cSJann Horn 			return 0;
1158068866cSJann Horn 		/*
1168068866cSJann Horn 		 * Reject use of CAP_SETUID for functionality other than calling
1178068866cSJann Horn 		 * set*uid() (e.g. setting up userns uid mappings).
118aeca4e2cSMicah Morton 		 */
119c783d525SJann Horn 		pr_warn("Operation requires CAP_SETUID, which is not available to UID %u for operations besides approved set*uid transitions\n",
120aeca4e2cSMicah Morton 			__kuid_val(cred->uid));
121e10337daSJann Horn 		return -EPERM;
1225294bac9SThomas Cedeno 	case CAP_SETGID:
1235294bac9SThomas Cedeno 		/*
1245294bac9SThomas Cedeno 		* If no policy applies to this task, allow the use of CAP_SETGID for
1255294bac9SThomas Cedeno 		* other purposes.
1265294bac9SThomas Cedeno 		*/
12703ca0ec1SThomas Cedeno 		if (setid_policy_lookup((kid_t){.gid = cred->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT)
1285294bac9SThomas Cedeno 			return 0;
1295294bac9SThomas Cedeno 		/*
1305294bac9SThomas Cedeno 		 * Reject use of CAP_SETUID for functionality other than calling
1315294bac9SThomas Cedeno 		 * set*gid() (e.g. setting up userns gid mappings).
1325294bac9SThomas Cedeno 		 */
1335294bac9SThomas Cedeno 		pr_warn("Operation requires CAP_SETGID, which is not available to GID %u for operations besides approved set*gid transitions\n",
134*970ebb8aSAlexander Mikhalitsyn 			__kgid_val(cred->gid));
1355294bac9SThomas Cedeno 		return -EPERM;
1365294bac9SThomas Cedeno 	default:
1375294bac9SThomas Cedeno 		/* Error, the only capabilities were checking for is CAP_SETUID/GID */
1385294bac9SThomas Cedeno 		return 0;
1395294bac9SThomas Cedeno 	}
1405294bac9SThomas Cedeno 	return 0;
141aeca4e2cSMicah Morton }
142aeca4e2cSMicah Morton 
143aeca4e2cSMicah Morton /*
1447ef6b306SJann Horn  * Check whether a caller with old credentials @old is allowed to switch to
1455294bac9SThomas Cedeno  * credentials that contain @new_id.
146aeca4e2cSMicah Morton  */
id_permitted_for_cred(const struct cred * old,kid_t new_id,enum setid_type new_type)1475294bac9SThomas Cedeno static bool id_permitted_for_cred(const struct cred *old, kid_t new_id, enum setid_type new_type)
1487ef6b306SJann Horn {
1497ef6b306SJann Horn 	bool permitted;
1507ef6b306SJann Horn 
1515294bac9SThomas Cedeno 	/* If our old creds already had this ID in it, it's fine. */
1525294bac9SThomas Cedeno 	if (new_type == UID) {
1535294bac9SThomas Cedeno 		if (uid_eq(new_id.uid, old->uid) || uid_eq(new_id.uid, old->euid) ||
1545294bac9SThomas Cedeno 			uid_eq(new_id.uid, old->suid))
1557ef6b306SJann Horn 			return true;
1565294bac9SThomas Cedeno 	} else if (new_type == GID){
1575294bac9SThomas Cedeno 		if (gid_eq(new_id.gid, old->gid) || gid_eq(new_id.gid, old->egid) ||
1585294bac9SThomas Cedeno 			gid_eq(new_id.gid, old->sgid))
1595294bac9SThomas Cedeno 			return true;
1605294bac9SThomas Cedeno 	} else /* Error, new_type is an invalid type */
1615294bac9SThomas Cedeno 		return false;
1627ef6b306SJann Horn 
1637ef6b306SJann Horn 	/*
1647ef6b306SJann Horn 	 * Transitions to new UIDs require a check against the policy of the old
1657ef6b306SJann Horn 	 * RUID.
1667ef6b306SJann Horn 	 */
1671cd02a27SJann Horn 	permitted =
16803ca0ec1SThomas Cedeno 	    setid_policy_lookup((kid_t){.uid = old->uid}, new_id, new_type) != SIDPOL_CONSTRAINED;
1695294bac9SThomas Cedeno 
1707ef6b306SJann Horn 	if (!permitted) {
1715294bac9SThomas Cedeno 		if (new_type == UID) {
1727ef6b306SJann Horn 			pr_warn("UID transition ((%d,%d,%d) -> %d) blocked\n",
1737ef6b306SJann Horn 				__kuid_val(old->uid), __kuid_val(old->euid),
1745294bac9SThomas Cedeno 				__kuid_val(old->suid), __kuid_val(new_id.uid));
1755294bac9SThomas Cedeno 		} else if (new_type == GID) {
1765294bac9SThomas Cedeno 			pr_warn("GID transition ((%d,%d,%d) -> %d) blocked\n",
1775294bac9SThomas Cedeno 				__kgid_val(old->gid), __kgid_val(old->egid),
1785294bac9SThomas Cedeno 				__kgid_val(old->sgid), __kgid_val(new_id.gid));
1795294bac9SThomas Cedeno 		} else /* Error, new_type is an invalid type */
1805294bac9SThomas Cedeno 			return false;
1817ef6b306SJann Horn 	}
1827ef6b306SJann Horn 	return permitted;
183aeca4e2cSMicah Morton }
184aeca4e2cSMicah Morton 
185aeca4e2cSMicah Morton /*
186aeca4e2cSMicah Morton  * Check whether there is either an exception for user under old cred struct to
187aeca4e2cSMicah Morton  * set*uid to user under new cred struct, or the UID transition is allowed (by
188aeca4e2cSMicah Morton  * Linux set*uid rules) even without CAP_SETUID.
189aeca4e2cSMicah Morton  */
safesetid_task_fix_setuid(struct cred * new,const struct cred * old,int flags)190aeca4e2cSMicah Morton static int safesetid_task_fix_setuid(struct cred *new,
191aeca4e2cSMicah Morton 				     const struct cred *old,
192aeca4e2cSMicah Morton 				     int flags)
193aeca4e2cSMicah Morton {
194aeca4e2cSMicah Morton 
1957ef6b306SJann Horn 	/* Do nothing if there are no setuid restrictions for our old RUID. */
19603ca0ec1SThomas Cedeno 	if (setid_policy_lookup((kid_t){.uid = old->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT)
197aeca4e2cSMicah Morton 		return 0;
198aeca4e2cSMicah Morton 
19903ca0ec1SThomas Cedeno 	if (id_permitted_for_cred(old, (kid_t){.uid = new->uid}, UID) &&
20003ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.uid = new->euid}, UID) &&
20103ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.uid = new->suid}, UID) &&
20203ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.uid = new->fsuid}, UID))
203aeca4e2cSMicah Morton 		return 0;
2047ef6b306SJann Horn 
2057ef6b306SJann Horn 	/*
2067ef6b306SJann Horn 	 * Kill this process to avoid potential security vulnerabilities
2075294bac9SThomas Cedeno 	 * that could arise from a missing allowlist entry preventing a
2085294bac9SThomas Cedeno 	 * privileged process from dropping to a lesser-privileged one.
2095294bac9SThomas Cedeno 	 */
2105294bac9SThomas Cedeno 	force_sig(SIGKILL);
2115294bac9SThomas Cedeno 	return -EACCES;
2125294bac9SThomas Cedeno }
2135294bac9SThomas Cedeno 
safesetid_task_fix_setgid(struct cred * new,const struct cred * old,int flags)2145294bac9SThomas Cedeno static int safesetid_task_fix_setgid(struct cred *new,
2155294bac9SThomas Cedeno 				     const struct cred *old,
2165294bac9SThomas Cedeno 				     int flags)
2175294bac9SThomas Cedeno {
2185294bac9SThomas Cedeno 
2195294bac9SThomas Cedeno 	/* Do nothing if there are no setgid restrictions for our old RGID. */
22003ca0ec1SThomas Cedeno 	if (setid_policy_lookup((kid_t){.gid = old->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT)
2215294bac9SThomas Cedeno 		return 0;
2225294bac9SThomas Cedeno 
22303ca0ec1SThomas Cedeno 	if (id_permitted_for_cred(old, (kid_t){.gid = new->gid}, GID) &&
22403ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.gid = new->egid}, GID) &&
22503ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.gid = new->sgid}, GID) &&
22603ca0ec1SThomas Cedeno 	    id_permitted_for_cred(old, (kid_t){.gid = new->fsgid}, GID))
2275294bac9SThomas Cedeno 		return 0;
2285294bac9SThomas Cedeno 
2295294bac9SThomas Cedeno 	/*
2305294bac9SThomas Cedeno 	 * Kill this process to avoid potential security vulnerabilities
2315294bac9SThomas Cedeno 	 * that could arise from a missing allowlist entry preventing a
2327ef6b306SJann Horn 	 * privileged process from dropping to a lesser-privileged one.
2337ef6b306SJann Horn 	 */
2347ef6b306SJann Horn 	force_sig(SIGKILL);
2357ef6b306SJann Horn 	return -EACCES;
236aeca4e2cSMicah Morton }
237aeca4e2cSMicah Morton 
safesetid_task_fix_setgroups(struct cred * new,const struct cred * old)2383e3374d3SMicah Morton static int safesetid_task_fix_setgroups(struct cred *new, const struct cred *old)
2393e3374d3SMicah Morton {
2403e3374d3SMicah Morton 	int i;
2413e3374d3SMicah Morton 
2423e3374d3SMicah Morton 	/* Do nothing if there are no setgid restrictions for our old RGID. */
2433e3374d3SMicah Morton 	if (setid_policy_lookup((kid_t){.gid = old->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT)
2443e3374d3SMicah Morton 		return 0;
2453e3374d3SMicah Morton 
2463e3374d3SMicah Morton 	get_group_info(new->group_info);
2473e3374d3SMicah Morton 	for (i = 0; i < new->group_info->ngroups; i++) {
2483e3374d3SMicah Morton 		if (!id_permitted_for_cred(old, (kid_t){.gid = new->group_info->gid[i]}, GID)) {
2493e3374d3SMicah Morton 			put_group_info(new->group_info);
2503e3374d3SMicah Morton 			/*
2513e3374d3SMicah Morton 			 * Kill this process to avoid potential security vulnerabilities
2523e3374d3SMicah Morton 			 * that could arise from a missing allowlist entry preventing a
2533e3374d3SMicah Morton 			 * privileged process from dropping to a lesser-privileged one.
2543e3374d3SMicah Morton 			 */
2553e3374d3SMicah Morton 			force_sig(SIGKILL);
2563e3374d3SMicah Morton 			return -EACCES;
2573e3374d3SMicah Morton 		}
2583e3374d3SMicah Morton 	}
2593e3374d3SMicah Morton 
2603e3374d3SMicah Morton 	put_group_info(new->group_info);
2613e3374d3SMicah Morton 	return 0;
2623e3374d3SMicah Morton }
2633e3374d3SMicah Morton 
264aeca4e2cSMicah Morton static struct security_hook_list safesetid_security_hooks[] = {
265aeca4e2cSMicah Morton 	LSM_HOOK_INIT(task_fix_setuid, safesetid_task_fix_setuid),
2665294bac9SThomas Cedeno 	LSM_HOOK_INIT(task_fix_setgid, safesetid_task_fix_setgid),
2673e3374d3SMicah Morton 	LSM_HOOK_INIT(task_fix_setgroups, safesetid_task_fix_setgroups),
268aeca4e2cSMicah Morton 	LSM_HOOK_INIT(capable, safesetid_security_capable)
269aeca4e2cSMicah Morton };
270aeca4e2cSMicah Morton 
safesetid_security_init(void)271aeca4e2cSMicah Morton static int __init safesetid_security_init(void)
272aeca4e2cSMicah Morton {
273aeca4e2cSMicah Morton 	security_add_hooks(safesetid_security_hooks,
274aeca4e2cSMicah Morton 			   ARRAY_SIZE(safesetid_security_hooks), "safesetid");
275aeca4e2cSMicah Morton 
276aeca4e2cSMicah Morton 	/* Report that SafeSetID successfully initialized */
277aeca4e2cSMicah Morton 	safesetid_initialized = 1;
278aeca4e2cSMicah Morton 
279aeca4e2cSMicah Morton 	return 0;
280aeca4e2cSMicah Morton }
281aeca4e2cSMicah Morton 
282aeca4e2cSMicah Morton DEFINE_LSM(safesetid_security_init) = {
283aeca4e2cSMicah Morton 	.init = safesetid_security_init,
284f67e20d2SMicah Morton 	.name = "safesetid",
285aeca4e2cSMicah Morton };
286