346 lines
8.6 KiB
C
346 lines
8.6 KiB
C
// 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);
|