diff options
Diffstat (limited to 'security/safesetid/securityfs.c')
-rw-r--r-- | security/safesetid/securityfs.c | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/security/safesetid/securityfs.c b/security/safesetid/securityfs.c new file mode 100644 index 000000000..25310468b --- /dev/null +++ b/security/safesetid/securityfs.c @@ -0,0 +1,345 @@ +// 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); |