| // SPDX-License-Identifier: GPL-2.0 | 
 | /* | 
 |  * SafeSetID Linux Security Module | 
 |  * | 
 |  * Author: Micah Morton <mortonm@chromium.org> | 
 |  * | 
 |  * Copyright (C) 2018 The Chromium OS Authors. | 
 |  * | 
 |  * This program is free software; you can redistribute it and/or modify | 
 |  * it under the terms of the GNU General Public License version 2, as | 
 |  * published by the Free Software Foundation. | 
 |  * | 
 |  */ | 
 |  | 
 | #define pr_fmt(fmt) "SafeSetID: " fmt | 
 |  | 
 | #include <linux/security.h> | 
 | #include <linux/cred.h> | 
 |  | 
 | #include "lsm.h" | 
 |  | 
 | static DEFINE_MUTEX(uid_policy_update_lock); | 
 | static DEFINE_MUTEX(gid_policy_update_lock); | 
 |  | 
 | /* | 
 |  * In the case the input buffer contains one or more invalid IDs, the kid_t | 
 |  * variables pointed to by @parent and @child will get updated but this | 
 |  * function will return an error. | 
 |  * Contents of @buf may be modified. | 
 |  */ | 
 | static int parse_policy_line(struct file *file, char *buf, | 
 | 	struct setid_rule *rule) | 
 | { | 
 | 	char *child_str; | 
 | 	int ret; | 
 | 	u32 parsed_parent, parsed_child; | 
 |  | 
 | 	/* Format of |buf| string should be <UID>:<UID> or <GID>:<GID> */ | 
 | 	child_str = strchr(buf, ':'); | 
 | 	if (child_str == NULL) | 
 | 		return -EINVAL; | 
 | 	*child_str = '\0'; | 
 | 	child_str++; | 
 |  | 
 | 	ret = kstrtou32(buf, 0, &parsed_parent); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	ret = kstrtou32(child_str, 0, &parsed_child); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	if (rule->type == UID){ | 
 | 		rule->src_id.uid = make_kuid(file->f_cred->user_ns, parsed_parent); | 
 | 		rule->dst_id.uid = make_kuid(file->f_cred->user_ns, parsed_child); | 
 | 		if (!uid_valid(rule->src_id.uid) || !uid_valid(rule->dst_id.uid)) | 
 | 			return -EINVAL; | 
 | 	} else if (rule->type == GID){ | 
 | 		rule->src_id.gid = make_kgid(file->f_cred->user_ns, parsed_parent); | 
 | 		rule->dst_id.gid = make_kgid(file->f_cred->user_ns, parsed_child); | 
 | 		if (!gid_valid(rule->src_id.gid) || !gid_valid(rule->dst_id.gid)) | 
 | 			return -EINVAL; | 
 | 	} else { | 
 | 		/* Error, rule->type is an invalid type */ | 
 | 		return -EINVAL; | 
 | 	} | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void __release_ruleset(struct rcu_head *rcu) | 
 | { | 
 | 	struct setid_ruleset *pol = | 
 | 		container_of(rcu, struct setid_ruleset, rcu); | 
 | 	int bucket; | 
 | 	struct setid_rule *rule; | 
 | 	struct hlist_node *tmp; | 
 |  | 
 | 	hash_for_each_safe(pol->rules, bucket, tmp, rule, next) | 
 | 		kfree(rule); | 
 | 	kfree(pol->policy_str); | 
 | 	kfree(pol); | 
 | } | 
 |  | 
 | static void release_ruleset(struct setid_ruleset *pol){ | 
 | 	call_rcu(&pol->rcu, __release_ruleset); | 
 | } | 
 |  | 
 | static void insert_rule(struct setid_ruleset *pol, struct setid_rule *rule) | 
 | { | 
 | 	if (pol->type == UID) | 
 | 		hash_add(pol->rules, &rule->next, __kuid_val(rule->src_id.uid)); | 
 | 	else if (pol->type == GID) | 
 | 		hash_add(pol->rules, &rule->next, __kgid_val(rule->src_id.gid)); | 
 | 	else /* Error, pol->type is neither UID or GID */ | 
 | 		return; | 
 | } | 
 |  | 
 | static int verify_ruleset(struct setid_ruleset *pol) | 
 | { | 
 | 	int bucket; | 
 | 	struct setid_rule *rule, *nrule; | 
 | 	int res = 0; | 
 |  | 
 | 	hash_for_each(pol->rules, bucket, rule, next) { | 
 | 		if (_setid_policy_lookup(pol, rule->dst_id, INVALID_ID) == SIDPOL_DEFAULT) { | 
 | 			if (pol->type == UID) { | 
 | 				pr_warn("insecure policy detected: uid %d is constrained but transitively unconstrained through uid %d\n", | 
 | 					__kuid_val(rule->src_id.uid), | 
 | 					__kuid_val(rule->dst_id.uid)); | 
 | 			} else if (pol->type == GID) { | 
 | 				pr_warn("insecure policy detected: gid %d is constrained but transitively unconstrained through gid %d\n", | 
 | 					__kgid_val(rule->src_id.gid), | 
 | 					__kgid_val(rule->dst_id.gid)); | 
 | 			} else { /* pol->type is an invalid type */ | 
 | 				res = -EINVAL; | 
 | 				return res; | 
 | 			} | 
 | 			res = -EINVAL; | 
 |  | 
 | 			/* fix it up */ | 
 | 			nrule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL); | 
 | 			if (!nrule) | 
 | 				return -ENOMEM; | 
 | 			if (pol->type == UID){ | 
 | 				nrule->src_id.uid = rule->dst_id.uid; | 
 | 				nrule->dst_id.uid = rule->dst_id.uid; | 
 | 				nrule->type = UID; | 
 | 			} else { /* pol->type must be GID if we've made it to here */ | 
 | 				nrule->src_id.gid = rule->dst_id.gid; | 
 | 				nrule->dst_id.gid = rule->dst_id.gid; | 
 | 				nrule->type = GID; | 
 | 			} | 
 | 			insert_rule(pol, nrule); | 
 | 		} | 
 | 	} | 
 | 	return res; | 
 | } | 
 |  | 
 | static ssize_t handle_policy_update(struct file *file, | 
 | 				    const char __user *ubuf, size_t len, enum setid_type policy_type) | 
 | { | 
 | 	struct setid_ruleset *pol; | 
 | 	char *buf, *p, *end; | 
 | 	int err; | 
 |  | 
 | 	pol = kmalloc(sizeof(struct setid_ruleset), GFP_KERNEL); | 
 | 	if (!pol) | 
 | 		return -ENOMEM; | 
 | 	pol->policy_str = NULL; | 
 | 	pol->type = policy_type; | 
 | 	hash_init(pol->rules); | 
 |  | 
 | 	p = buf = memdup_user_nul(ubuf, len); | 
 | 	if (IS_ERR(buf)) { | 
 | 		err = PTR_ERR(buf); | 
 | 		goto out_free_pol; | 
 | 	} | 
 | 	pol->policy_str = kstrdup(buf, GFP_KERNEL); | 
 | 	if (pol->policy_str == NULL) { | 
 | 		err = -ENOMEM; | 
 | 		goto out_free_buf; | 
 | 	} | 
 |  | 
 | 	/* policy lines, including the last one, end with \n */ | 
 | 	while (*p != '\0') { | 
 | 		struct setid_rule *rule; | 
 |  | 
 | 		end = strchr(p, '\n'); | 
 | 		if (end == NULL) { | 
 | 			err = -EINVAL; | 
 | 			goto out_free_buf; | 
 | 		} | 
 | 		*end = '\0'; | 
 |  | 
 | 		rule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL); | 
 | 		if (!rule) { | 
 | 			err = -ENOMEM; | 
 | 			goto out_free_buf; | 
 | 		} | 
 |  | 
 | 		rule->type = policy_type; | 
 | 		err = parse_policy_line(file, p, rule); | 
 | 		if (err) | 
 | 			goto out_free_rule; | 
 |  | 
 | 		if (_setid_policy_lookup(pol, rule->src_id, rule->dst_id) == SIDPOL_ALLOWED) { | 
 | 			pr_warn("bad policy: duplicate entry\n"); | 
 | 			err = -EEXIST; | 
 | 			goto out_free_rule; | 
 | 		} | 
 |  | 
 | 		insert_rule(pol, rule); | 
 | 		p = end + 1; | 
 | 		continue; | 
 |  | 
 | out_free_rule: | 
 | 		kfree(rule); | 
 | 		goto out_free_buf; | 
 | 	} | 
 |  | 
 | 	err = verify_ruleset(pol); | 
 | 	/* bogus policy falls through after fixing it up */ | 
 | 	if (err && err != -EINVAL) | 
 | 		goto out_free_buf; | 
 |  | 
 | 	/* | 
 | 	 * Everything looks good, apply the policy and release the old one. | 
 | 	 * What we really want here is an xchg() wrapper for RCU, but since that | 
 | 	 * doesn't currently exist, just use a spinlock for now. | 
 | 	 */ | 
 | 	if (policy_type == UID) { | 
 | 		mutex_lock(&uid_policy_update_lock); | 
 | 		pol = rcu_replace_pointer(safesetid_setuid_rules, pol, | 
 | 					  lockdep_is_held(&uid_policy_update_lock)); | 
 | 		mutex_unlock(&uid_policy_update_lock); | 
 | 	} else if (policy_type == GID) { | 
 | 		mutex_lock(&gid_policy_update_lock); | 
 | 		pol = rcu_replace_pointer(safesetid_setgid_rules, pol, | 
 | 					  lockdep_is_held(&gid_policy_update_lock)); | 
 | 		mutex_unlock(&gid_policy_update_lock); | 
 | 	} else { | 
 | 		/* Error, policy type is neither UID or GID */ | 
 | 		pr_warn("error: bad policy type"); | 
 | 	} | 
 | 	err = len; | 
 |  | 
 | out_free_buf: | 
 | 	kfree(buf); | 
 | out_free_pol: | 
 | 	if (pol) | 
 | 		release_ruleset(pol); | 
 | 	return err; | 
 | } | 
 |  | 
 | static ssize_t safesetid_uid_file_write(struct file *file, | 
 | 				    const char __user *buf, | 
 | 				    size_t len, | 
 | 				    loff_t *ppos) | 
 | { | 
 | 	if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) | 
 | 		return -EPERM; | 
 |  | 
 | 	if (*ppos != 0) | 
 | 		return -EINVAL; | 
 |  | 
 | 	return handle_policy_update(file, buf, len, UID); | 
 | } | 
 |  | 
 | static ssize_t safesetid_gid_file_write(struct file *file, | 
 | 				    const char __user *buf, | 
 | 				    size_t len, | 
 | 				    loff_t *ppos) | 
 | { | 
 | 	if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) | 
 | 		return -EPERM; | 
 |  | 
 | 	if (*ppos != 0) | 
 | 		return -EINVAL; | 
 |  | 
 | 	return handle_policy_update(file, buf, len, GID); | 
 | } | 
 |  | 
 | static ssize_t safesetid_file_read(struct file *file, char __user *buf, | 
 | 				   size_t len, loff_t *ppos, struct mutex *policy_update_lock, struct __rcu setid_ruleset* ruleset) | 
 | { | 
 | 	ssize_t res = 0; | 
 | 	struct setid_ruleset *pol; | 
 | 	const char *kbuf; | 
 |  | 
 | 	mutex_lock(policy_update_lock); | 
 | 	pol = rcu_dereference_protected(ruleset, lockdep_is_held(policy_update_lock)); | 
 | 	if (pol) { | 
 | 		kbuf = pol->policy_str; | 
 | 		res = simple_read_from_buffer(buf, len, ppos, | 
 | 					      kbuf, strlen(kbuf)); | 
 | 	} | 
 | 	mutex_unlock(policy_update_lock); | 
 |  | 
 | 	return res; | 
 | } | 
 |  | 
 | static ssize_t safesetid_uid_file_read(struct file *file, char __user *buf, | 
 | 				   size_t len, loff_t *ppos) | 
 | { | 
 | 	return safesetid_file_read(file, buf, len, ppos, | 
 | 				   &uid_policy_update_lock, safesetid_setuid_rules); | 
 | } | 
 |  | 
 | static ssize_t safesetid_gid_file_read(struct file *file, char __user *buf, | 
 | 				   size_t len, loff_t *ppos) | 
 | { | 
 | 	return safesetid_file_read(file, buf, len, ppos, | 
 | 				   &gid_policy_update_lock, safesetid_setgid_rules); | 
 | } | 
 |  | 
 |  | 
 |  | 
 | static const struct file_operations safesetid_uid_file_fops = { | 
 | 	.read = safesetid_uid_file_read, | 
 | 	.write = safesetid_uid_file_write, | 
 | }; | 
 |  | 
 | static const struct file_operations safesetid_gid_file_fops = { | 
 | 	.read = safesetid_gid_file_read, | 
 | 	.write = safesetid_gid_file_write, | 
 | }; | 
 |  | 
 | static int __init safesetid_init_securityfs(void) | 
 | { | 
 | 	int ret; | 
 | 	struct dentry *policy_dir; | 
 | 	struct dentry *uid_policy_file; | 
 | 	struct dentry *gid_policy_file; | 
 |  | 
 | 	if (!safesetid_initialized) | 
 | 		return 0; | 
 |  | 
 | 	policy_dir = securityfs_create_dir("safesetid", NULL); | 
 | 	if (IS_ERR(policy_dir)) { | 
 | 		ret = PTR_ERR(policy_dir); | 
 | 		goto error; | 
 | 	} | 
 |  | 
 | 	uid_policy_file = securityfs_create_file("uid_allowlist_policy", 0600, | 
 | 			policy_dir, NULL, &safesetid_uid_file_fops); | 
 | 	if (IS_ERR(uid_policy_file)) { | 
 | 		ret = PTR_ERR(uid_policy_file); | 
 | 		goto error; | 
 | 	} | 
 |  | 
 | 	gid_policy_file = securityfs_create_file("gid_allowlist_policy", 0600, | 
 | 			policy_dir, NULL, &safesetid_gid_file_fops); | 
 | 	if (IS_ERR(gid_policy_file)) { | 
 | 		ret = PTR_ERR(gid_policy_file); | 
 | 		goto error; | 
 | 	} | 
 |  | 
 |  | 
 | 	return 0; | 
 |  | 
 | error: | 
 | 	securityfs_remove(policy_dir); | 
 | 	return ret; | 
 | } | 
 | fs_initcall(safesetid_init_securityfs); |