root/security/safesetid/securityfs.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_obj(struct setid_rule);
                        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;

        if (len >= KMALLOC_MAX_SIZE)
                return -EINVAL;

        pol = kmalloc_obj(struct setid_ruleset);
        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_obj(struct setid_rule);
                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,
};

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;
}