| // 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_obj(struct setid_rule); | |
| 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; | |
| if (len >= KMALLOC_MAX_SIZE) | |
| return -EINVAL; | |
| pol = kmalloc_obj(struct setid_ruleset); | |
| 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_obj(struct setid_rule); | |
| 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, | |
| }; | |
| 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; | |
| } |