diff options
Diffstat (limited to '')
-rw-r--r-- | security/safesetid/Kconfig | 15 | ||||
-rw-r--r-- | security/safesetid/Makefile | 7 | ||||
-rw-r--r-- | security/safesetid/lsm.c | 285 | ||||
-rw-r--r-- | security/safesetid/lsm.h | 73 | ||||
-rw-r--r-- | security/safesetid/securityfs.c | 345 |
5 files changed, 725 insertions, 0 deletions
diff --git a/security/safesetid/Kconfig b/security/safesetid/Kconfig new file mode 100644 index 000000000..18b5fb904 --- /dev/null +++ b/security/safesetid/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only +config SECURITY_SAFESETID + bool "Gate setid transitions to limit CAP_SET{U/G}ID capabilities" + depends on SECURITY + select SECURITYFS + default n + help + SafeSetID is an LSM module that gates the setid family of syscalls to + restrict UID/GID transitions from a given UID/GID to only those + approved by a system-wide whitelist. These restrictions also prohibit + the given UIDs/GIDs from obtaining auxiliary privileges associated + with CAP_SET{U/G}ID, such as allowing a user to set up user namespace + UID mappings. + + If you are unsure how to answer this question, answer N. diff --git a/security/safesetid/Makefile b/security/safesetid/Makefile new file mode 100644 index 000000000..6b0660321 --- /dev/null +++ b/security/safesetid/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for the safesetid LSM. +# + +obj-$(CONFIG_SECURITY_SAFESETID) := safesetid.o +safesetid-y := lsm.o securityfs.o diff --git a/security/safesetid/lsm.c b/security/safesetid/lsm.c new file mode 100644 index 000000000..e806739f7 --- /dev/null +++ b/security/safesetid/lsm.c @@ -0,0 +1,285 @@ +// 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/lsm_hooks.h> +#include <linux/module.h> +#include <linux/ptrace.h> +#include <linux/sched/task_stack.h> +#include <linux/security.h> +#include "lsm.h" + +/* Flag indicating whether initialization completed */ +int safesetid_initialized __initdata; + +struct setid_ruleset __rcu *safesetid_setuid_rules; +struct setid_ruleset __rcu *safesetid_setgid_rules; + + +/* Compute a decision for a transition from @src to @dst under @policy. */ +enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy, + kid_t src, kid_t dst) +{ + struct setid_rule *rule; + enum sid_policy_type result = SIDPOL_DEFAULT; + + if (policy->type == UID) { + hash_for_each_possible(policy->rules, rule, next, __kuid_val(src.uid)) { + if (!uid_eq(rule->src_id.uid, src.uid)) + continue; + if (uid_eq(rule->dst_id.uid, dst.uid)) + return SIDPOL_ALLOWED; + result = SIDPOL_CONSTRAINED; + } + } else if (policy->type == GID) { + hash_for_each_possible(policy->rules, rule, next, __kgid_val(src.gid)) { + if (!gid_eq(rule->src_id.gid, src.gid)) + continue; + if (gid_eq(rule->dst_id.gid, dst.gid)){ + return SIDPOL_ALLOWED; + } + result = SIDPOL_CONSTRAINED; + } + } else { + /* Should not reach here, report the ID as contrainsted */ + result = SIDPOL_CONSTRAINED; + } + return result; +} + +/* + * Compute a decision for a transition from @src to @dst under the active + * policy. + */ +static enum sid_policy_type setid_policy_lookup(kid_t src, kid_t dst, enum setid_type new_type) +{ + enum sid_policy_type result = SIDPOL_DEFAULT; + struct setid_ruleset *pol; + + rcu_read_lock(); + if (new_type == UID) + pol = rcu_dereference(safesetid_setuid_rules); + else if (new_type == GID) + pol = rcu_dereference(safesetid_setgid_rules); + else { /* Should not reach here */ + result = SIDPOL_CONSTRAINED; + rcu_read_unlock(); + return result; + } + + if (pol) { + pol->type = new_type; + result = _setid_policy_lookup(pol, src, dst); + } + rcu_read_unlock(); + return result; +} + +static int safesetid_security_capable(const struct cred *cred, + struct user_namespace *ns, + int cap, + unsigned int opts) +{ + /* We're only interested in CAP_SETUID and CAP_SETGID. */ + if (cap != CAP_SETUID && cap != CAP_SETGID) + return 0; + + /* + * If CAP_SET{U/G}ID is currently used for a setid or setgroups syscall, we + * want to let it go through here; the real security check happens later, in + * the task_fix_set{u/g}id or task_fix_setgroups hooks. + */ + if ((opts & CAP_OPT_INSETID) != 0) + return 0; + + switch (cap) { + case CAP_SETUID: + /* + * If no policy applies to this task, allow the use of CAP_SETUID for + * other purposes. + */ + if (setid_policy_lookup((kid_t){.uid = cred->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT) + return 0; + /* + * Reject use of CAP_SETUID for functionality other than calling + * set*uid() (e.g. setting up userns uid mappings). + */ + pr_warn("Operation requires CAP_SETUID, which is not available to UID %u for operations besides approved set*uid transitions\n", + __kuid_val(cred->uid)); + return -EPERM; + case CAP_SETGID: + /* + * If no policy applies to this task, allow the use of CAP_SETGID for + * other purposes. + */ + if (setid_policy_lookup((kid_t){.gid = cred->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT) + return 0; + /* + * Reject use of CAP_SETUID for functionality other than calling + * set*gid() (e.g. setting up userns gid mappings). + */ + pr_warn("Operation requires CAP_SETGID, which is not available to GID %u for operations besides approved set*gid transitions\n", + __kuid_val(cred->uid)); + return -EPERM; + default: + /* Error, the only capabilities were checking for is CAP_SETUID/GID */ + return 0; + } + return 0; +} + +/* + * Check whether a caller with old credentials @old is allowed to switch to + * credentials that contain @new_id. + */ +static bool id_permitted_for_cred(const struct cred *old, kid_t new_id, enum setid_type new_type) +{ + bool permitted; + + /* If our old creds already had this ID in it, it's fine. */ + if (new_type == UID) { + if (uid_eq(new_id.uid, old->uid) || uid_eq(new_id.uid, old->euid) || + uid_eq(new_id.uid, old->suid)) + return true; + } else if (new_type == GID){ + if (gid_eq(new_id.gid, old->gid) || gid_eq(new_id.gid, old->egid) || + gid_eq(new_id.gid, old->sgid)) + return true; + } else /* Error, new_type is an invalid type */ + return false; + + /* + * Transitions to new UIDs require a check against the policy of the old + * RUID. + */ + permitted = + setid_policy_lookup((kid_t){.uid = old->uid}, new_id, new_type) != SIDPOL_CONSTRAINED; + + if (!permitted) { + if (new_type == UID) { + pr_warn("UID transition ((%d,%d,%d) -> %d) blocked\n", + __kuid_val(old->uid), __kuid_val(old->euid), + __kuid_val(old->suid), __kuid_val(new_id.uid)); + } else if (new_type == GID) { + pr_warn("GID transition ((%d,%d,%d) -> %d) blocked\n", + __kgid_val(old->gid), __kgid_val(old->egid), + __kgid_val(old->sgid), __kgid_val(new_id.gid)); + } else /* Error, new_type is an invalid type */ + return false; + } + return permitted; +} + +/* + * Check whether there is either an exception for user under old cred struct to + * set*uid to user under new cred struct, or the UID transition is allowed (by + * Linux set*uid rules) even without CAP_SETUID. + */ +static int safesetid_task_fix_setuid(struct cred *new, + const struct cred *old, + int flags) +{ + + /* Do nothing if there are no setuid restrictions for our old RUID. */ + if (setid_policy_lookup((kid_t){.uid = old->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT) + return 0; + + if (id_permitted_for_cred(old, (kid_t){.uid = new->uid}, UID) && + id_permitted_for_cred(old, (kid_t){.uid = new->euid}, UID) && + id_permitted_for_cred(old, (kid_t){.uid = new->suid}, UID) && + id_permitted_for_cred(old, (kid_t){.uid = new->fsuid}, UID)) + return 0; + + /* + * Kill this process to avoid potential security vulnerabilities + * that could arise from a missing allowlist entry preventing a + * privileged process from dropping to a lesser-privileged one. + */ + force_sig(SIGKILL); + return -EACCES; +} + +static int safesetid_task_fix_setgid(struct cred *new, + const struct cred *old, + int flags) +{ + + /* Do nothing if there are no setgid restrictions for our old RGID. */ + if (setid_policy_lookup((kid_t){.gid = old->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT) + return 0; + + if (id_permitted_for_cred(old, (kid_t){.gid = new->gid}, GID) && + id_permitted_for_cred(old, (kid_t){.gid = new->egid}, GID) && + id_permitted_for_cred(old, (kid_t){.gid = new->sgid}, GID) && + id_permitted_for_cred(old, (kid_t){.gid = new->fsgid}, GID)) + return 0; + + /* + * Kill this process to avoid potential security vulnerabilities + * that could arise from a missing allowlist entry preventing a + * privileged process from dropping to a lesser-privileged one. + */ + force_sig(SIGKILL); + return -EACCES; +} + +static int safesetid_task_fix_setgroups(struct cred *new, const struct cred *old) +{ + int i; + + /* Do nothing if there are no setgid restrictions for our old RGID. */ + if (setid_policy_lookup((kid_t){.gid = old->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT) + return 0; + + get_group_info(new->group_info); + for (i = 0; i < new->group_info->ngroups; i++) { + if (!id_permitted_for_cred(old, (kid_t){.gid = new->group_info->gid[i]}, GID)) { + put_group_info(new->group_info); + /* + * Kill this process to avoid potential security vulnerabilities + * that could arise from a missing allowlist entry preventing a + * privileged process from dropping to a lesser-privileged one. + */ + force_sig(SIGKILL); + return -EACCES; + } + } + + put_group_info(new->group_info); + return 0; +} + +static struct security_hook_list safesetid_security_hooks[] = { + LSM_HOOK_INIT(task_fix_setuid, safesetid_task_fix_setuid), + LSM_HOOK_INIT(task_fix_setgid, safesetid_task_fix_setgid), + LSM_HOOK_INIT(task_fix_setgroups, safesetid_task_fix_setgroups), + LSM_HOOK_INIT(capable, safesetid_security_capable) +}; + +static int __init safesetid_security_init(void) +{ + security_add_hooks(safesetid_security_hooks, + ARRAY_SIZE(safesetid_security_hooks), "safesetid"); + + /* Report that SafeSetID successfully initialized */ + safesetid_initialized = 1; + + return 0; +} + +DEFINE_LSM(safesetid_security_init) = { + .init = safesetid_security_init, + .name = "safesetid", +}; diff --git a/security/safesetid/lsm.h b/security/safesetid/lsm.h new file mode 100644 index 000000000..d346f4849 --- /dev/null +++ b/security/safesetid/lsm.h @@ -0,0 +1,73 @@ +/* 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. + * + */ +#ifndef _SAFESETID_H +#define _SAFESETID_H + +#include <linux/types.h> +#include <linux/uidgid.h> +#include <linux/hashtable.h> + +/* Flag indicating whether initialization completed */ +extern int safesetid_initialized __initdata; + +enum sid_policy_type { + SIDPOL_DEFAULT, /* source ID is unaffected by policy */ + SIDPOL_CONSTRAINED, /* source ID is affected by policy */ + SIDPOL_ALLOWED /* target ID explicitly allowed */ +}; + +typedef union { + kuid_t uid; + kgid_t gid; +} kid_t; + +enum setid_type { + UID, + GID +}; + +/* + * Hash table entry to store safesetid policy signifying that 'src_id' + * can set*id to 'dst_id'. + */ +struct setid_rule { + struct hlist_node next; + kid_t src_id; + kid_t dst_id; + + /* Flag to signal if rule is for UID's or GID's */ + enum setid_type type; +}; + +#define SETID_HASH_BITS 8 /* 256 buckets in hash table */ + +/* Extension of INVALID_UID/INVALID_GID for kid_t type */ +#define INVALID_ID (kid_t){.uid = INVALID_UID} + +struct setid_ruleset { + DECLARE_HASHTABLE(rules, SETID_HASH_BITS); + char *policy_str; + struct rcu_head rcu; + + //Flag to signal if ruleset is for UID's or GID's + enum setid_type type; +}; + +enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy, + kid_t src, kid_t dst); + +extern struct setid_ruleset __rcu *safesetid_setuid_rules; +extern struct setid_ruleset __rcu *safesetid_setgid_rules; + +#endif /* _SAFESETID_H */ diff --git a/security/safesetid/securityfs.c b/security/safesetid/securityfs.c new file mode 100644 index 000000000..25310468b --- /dev/null +++ b/security/safesetid/securityfs.c @@ -0,0 +1,345 @@ +// 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); |