// 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);