// SPDX-License-Identifier: GPL-2.0 /* * SafeSetID Linux Security Module * * Author: Micah Morton * * 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 #include #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 : or : */ 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);