| // 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(policy_update_lock); |
| |
| /* |
| * In the case the input buffer contains one or more invalid UIDs, the kuid_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 setuid_rule *rule) |
| { |
| char *child_str; |
| int ret; |
| u32 parsed_parent, parsed_child; |
| |
| /* Format of |buf| string should be <UID>:<UID>. */ |
| 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; |
| |
| rule->src_uid = make_kuid(file->f_cred->user_ns, parsed_parent); |
| rule->dst_uid = make_kuid(file->f_cred->user_ns, parsed_child); |
| if (!uid_valid(rule->src_uid) || !uid_valid(rule->dst_uid)) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static void __release_ruleset(struct rcu_head *rcu) |
| { |
| struct setuid_ruleset *pol = |
| container_of(rcu, struct setuid_ruleset, rcu); |
| int bucket; |
| struct setuid_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 setuid_ruleset *pol) |
| { |
| call_rcu(&pol->rcu, __release_ruleset); |
| } |
| |
| static void insert_rule(struct setuid_ruleset *pol, struct setuid_rule *rule) |
| { |
| hash_add(pol->rules, &rule->next, __kuid_val(rule->src_uid)); |
| } |
| |
| static int verify_ruleset(struct setuid_ruleset *pol) |
| { |
| int bucket; |
| struct setuid_rule *rule, *nrule; |
| int res = 0; |
| |
| hash_for_each(pol->rules, bucket, rule, next) { |
| if (_setuid_policy_lookup(pol, rule->dst_uid, INVALID_UID) == |
| SIDPOL_DEFAULT) { |
| pr_warn("insecure policy detected: uid %d is constrained but transitively unconstrained through uid %d\n", |
| __kuid_val(rule->src_uid), |
| __kuid_val(rule->dst_uid)); |
| res = -EINVAL; |
| |
| /* fix it up */ |
| nrule = kmalloc(sizeof(struct setuid_rule), GFP_KERNEL); |
| if (!nrule) |
| return -ENOMEM; |
| nrule->src_uid = rule->dst_uid; |
| nrule->dst_uid = rule->dst_uid; |
| insert_rule(pol, nrule); |
| } |
| } |
| return res; |
| } |
| |
| static ssize_t handle_policy_update(struct file *file, |
| const char __user *ubuf, size_t len) |
| { |
| struct setuid_ruleset *pol; |
| char *buf, *p, *end; |
| int err; |
| |
| pol = kmalloc(sizeof(struct setuid_ruleset), GFP_KERNEL); |
| if (!pol) |
| return -ENOMEM; |
| pol->policy_str = NULL; |
| 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 setuid_rule *rule; |
| |
| end = strchr(p, '\n'); |
| if (end == NULL) { |
| err = -EINVAL; |
| goto out_free_buf; |
| } |
| *end = '\0'; |
| |
| rule = kmalloc(sizeof(struct setuid_rule), GFP_KERNEL); |
| if (!rule) { |
| err = -ENOMEM; |
| goto out_free_buf; |
| } |
| |
| err = parse_policy_line(file, p, rule); |
| if (err) |
| goto out_free_rule; |
| |
| if (_setuid_policy_lookup(pol, rule->src_uid, rule->dst_uid) == |
| 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. |
| */ |
| mutex_lock(&policy_update_lock); |
| rcu_swap_protected(safesetid_setuid_rules, pol, |
| lockdep_is_held(&policy_update_lock)); |
| mutex_unlock(&policy_update_lock); |
| err = len; |
| |
| out_free_buf: |
| kfree(buf); |
| out_free_pol: |
| if (pol) |
| release_ruleset(pol); |
| return err; |
| } |
| |
| static ssize_t safesetid_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); |
| } |
| |
| static ssize_t safesetid_file_read(struct file *file, char __user *buf, |
| size_t len, loff_t *ppos) |
| { |
| ssize_t res = 0; |
| struct setuid_ruleset *pol; |
| const char *kbuf; |
| |
| mutex_lock(&policy_update_lock); |
| pol = rcu_dereference_protected(safesetid_setuid_rules, |
| 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 const struct file_operations safesetid_file_fops = { |
| .read = safesetid_file_read, |
| .write = safesetid_file_write, |
| }; |
| |
| static int __init safesetid_init_securityfs(void) |
| { |
| int ret; |
| struct dentry *policy_dir; |
| struct dentry *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; |
| } |
| |
| policy_file = securityfs_create_file("whitelist_policy", 0600, |
| policy_dir, NULL, &safesetid_file_fops); |
| if (IS_ERR(policy_file)) { |
| ret = PTR_ERR(policy_file); |
| goto error; |
| } |
| |
| return 0; |
| |
| error: |
| securityfs_remove(policy_dir); |
| return ret; |
| } |
| fs_initcall(safesetid_init_securityfs); |