diff options
Diffstat (limited to 'security')
-rw-r--r-- | security/safesetid/lsm.c | 190 | ||||
-rw-r--r-- | security/safesetid/lsm.h | 38 | ||||
-rw-r--r-- | security/safesetid/securityfs.c | 190 |
3 files changed, 309 insertions, 109 deletions
diff --git a/security/safesetid/lsm.c b/security/safesetid/lsm.c index 7760019ad35d..c08e67108a82 100644 --- a/security/safesetid/lsm.c +++ b/security/safesetid/lsm.c @@ -24,20 +24,36 @@ /* Flag indicating whether initialization completed */ int safesetid_initialized; -struct setuid_ruleset __rcu *safesetid_setuid_rules; +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 _setuid_policy_lookup(struct setuid_ruleset *policy, - kuid_t src, kuid_t dst) +enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy, + kid_t src, kid_t dst) { - struct setuid_rule *rule; + struct setid_rule *rule; enum sid_policy_type result = SIDPOL_DEFAULT; - hash_for_each_possible(policy->rules, rule, next, __kuid_val(src)) { - if (!uid_eq(rule->src_uid, src)) - continue; - if (uid_eq(rule->dst_uid, dst)) - return SIDPOL_ALLOWED; + 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; @@ -47,15 +63,26 @@ enum sid_policy_type _setuid_policy_lookup(struct setuid_ruleset *policy, * Compute a decision for a transition from @src to @dst under the active * policy. */ -static enum sid_policy_type setuid_policy_lookup(kuid_t src, kuid_t dst) +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 setuid_ruleset *pol; + struct setid_ruleset *pol; rcu_read_lock(); - pol = rcu_dereference(safesetid_setuid_rules); - if (pol) - result = _setuid_policy_lookup(pol, src, dst); + 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; } @@ -65,57 +92,101 @@ static int safesetid_security_capable(const struct cred *cred, int cap, unsigned int opts) { - /* We're only interested in CAP_SETUID. */ - if (cap != CAP_SETUID) + /* We're only interested in CAP_SETUID and CAP_SETGID. */ + if (cap != CAP_SETUID && cap != CAP_SETGID) return 0; /* - * If CAP_SETUID is currently used for a set*uid() syscall, we want to + * If CAP_SET{U/G}ID is currently used for a setid() syscall, we want to * let it go through here; the real security check happens later, in the - * task_fix_setuid hook. + * task_fix_set{u/g}id hook. + * + * NOTE: + * Until we add support for restricting setgroups() calls, GID security + * policies offer no meaningful security since we always return 0 here + * when called from within the setgroups() syscall and there is no + * additional hook later on to enforce security policies for setgroups(). */ if ((opts & CAP_OPT_INSETID) != 0) return 0; - /* - * If no policy applies to this task, allow the use of CAP_SETUID for - * other purposes. - */ - if (setuid_policy_lookup(cred->uid, INVALID_UID) == SIDPOL_DEFAULT) + 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)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; + break; + 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)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; + break; + default: + /* Error, the only capabilities were checking for is CAP_SETUID/GID */ 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; + break; + } + return 0; } /* * Check whether a caller with old credentials @old is allowed to switch to - * credentials that contain @new_uid. + * credentials that contain @new_id. */ -static bool uid_permitted_for_cred(const struct cred *old, kuid_t new_uid) +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 UID in it, it's fine. */ - if (uid_eq(new_uid, old->uid) || uid_eq(new_uid, old->euid) || - uid_eq(new_uid, old->suid)) - return true; + /* 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 = - setuid_policy_lookup(old->uid, new_uid) != SIDPOL_CONSTRAINED; + setid_policy_lookup((kid_t)old->uid, new_id, new_type) != SIDPOL_CONSTRAINED; + if (!permitted) { - 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_uid)); + 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; } @@ -131,18 +202,42 @@ static int safesetid_task_fix_setuid(struct cred *new, { /* Do nothing if there are no setuid restrictions for our old RUID. */ - if (setuid_policy_lookup(old->uid, INVALID_UID) == SIDPOL_DEFAULT) + if (setid_policy_lookup((kid_t)old->uid, INVALID_ID, UID) == SIDPOL_DEFAULT) + return 0; + + if (id_permitted_for_cred(old, (kid_t)new->uid, UID) && + id_permitted_for_cred(old, (kid_t)new->euid, UID) && + id_permitted_for_cred(old, (kid_t)new->suid, UID) && + id_permitted_for_cred(old, (kid_t)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)old->gid, INVALID_ID, GID) == SIDPOL_DEFAULT) return 0; - if (uid_permitted_for_cred(old, new->uid) && - uid_permitted_for_cred(old, new->euid) && - uid_permitted_for_cred(old, new->suid) && - uid_permitted_for_cred(old, new->fsuid)) + if (id_permitted_for_cred(old, (kid_t)new->gid, GID) && + id_permitted_for_cred(old, (kid_t)new->egid, GID) && + id_permitted_for_cred(old, (kid_t)new->sgid, GID) && + id_permitted_for_cred(old, (kid_t)new->fsgid, GID)) return 0; /* * Kill this process to avoid potential security vulnerabilities - * that could arise from a missing whitelist entry preventing a + * that could arise from a missing allowlist entry preventing a * privileged process from dropping to a lesser-privileged one. */ force_sig(SIGKILL); @@ -151,6 +246,7 @@ static int safesetid_task_fix_setuid(struct cred *new, 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(capable, safesetid_security_capable) }; diff --git a/security/safesetid/lsm.h b/security/safesetid/lsm.h index db6d16e6bbc3..bde8c43a3767 100644 --- a/security/safesetid/lsm.h +++ b/security/safesetid/lsm.h @@ -27,27 +27,47 @@ enum sid_policy_type { 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_uid' - * can setuid to 'dst_uid'. + * Hash table entry to store safesetid policy signifying that 'src_id' + * can set*id to 'dst_id'. */ -struct setuid_rule { +struct setid_rule { struct hlist_node next; - kuid_t src_uid; - kuid_t dst_uid; + 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 */ -struct setuid_ruleset { +/* 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 _setuid_policy_lookup(struct setuid_ruleset *policy, - kuid_t src, kuid_t dst); +enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy, + kid_t src, kid_t dst); -extern struct setuid_ruleset __rcu *safesetid_setuid_rules; +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 index f8bc574cea9c..642139008d42 100644 --- a/security/safesetid/securityfs.c +++ b/security/safesetid/securityfs.c @@ -19,22 +19,23 @@ #include "lsm.h" -static DEFINE_MUTEX(policy_update_lock); +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 UIDs, the kuid_t + * 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 setuid_rule *rule) + struct setid_rule *rule) { char *child_str; int ret; u32 parsed_parent, parsed_child; - /* Format of |buf| string should be <UID>:<UID>. */ + /* Format of |buf| string should be <UID>:<UID> or <GID>:<GID> */ child_str = strchr(buf, ':'); if (child_str == NULL) return -EINVAL; @@ -49,20 +50,29 @@ static int parse_policy_line(struct file *file, char *buf, 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)) + 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 setuid_ruleset *pol = - container_of(rcu, struct setuid_ruleset, rcu); + struct setid_ruleset *pol = + container_of(rcu, struct setid_ruleset, rcu); int bucket; - struct setuid_rule *rule; + struct setid_rule *rule; struct hlist_node *tmp; hash_for_each_safe(pol->rules, bucket, tmp, rule, next) @@ -71,36 +81,55 @@ static void __release_ruleset(struct rcu_head *rcu) kfree(pol); } -static void release_ruleset(struct setuid_ruleset *pol) -{ +static void release_ruleset(struct setid_ruleset *pol){ call_rcu(&pol->rcu, __release_ruleset); } -static void insert_rule(struct setuid_ruleset *pol, struct setuid_rule *rule) +static void insert_rule(struct setid_ruleset *pol, struct setid_rule *rule) { - hash_add(pol->rules, &rule->next, __kuid_val(rule->src_uid)); + 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 setuid_ruleset *pol) +static int verify_ruleset(struct setid_ruleset *pol) { int bucket; - struct setuid_rule *rule, *nrule; + struct setid_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)); + 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 setuid_rule), GFP_KERNEL); + nrule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL); if (!nrule) return -ENOMEM; - nrule->src_uid = rule->dst_uid; - nrule->dst_uid = rule->dst_uid; + 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); } } @@ -108,16 +137,17 @@ static int verify_ruleset(struct setuid_ruleset *pol) } static ssize_t handle_policy_update(struct file *file, - const char __user *ubuf, size_t len) + const char __user *ubuf, size_t len, enum setid_type policy_type) { - struct setuid_ruleset *pol; + struct setid_ruleset *pol; char *buf, *p, *end; int err; - pol = kmalloc(sizeof(struct setuid_ruleset), GFP_KERNEL); + 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); @@ -133,7 +163,7 @@ static ssize_t handle_policy_update(struct file *file, /* policy lines, including the last one, end with \n */ while (*p != '\0') { - struct setuid_rule *rule; + struct setid_rule *rule; end = strchr(p, '\n'); if (end == NULL) { @@ -142,18 +172,18 @@ static ssize_t handle_policy_update(struct file *file, } *end = '\0'; - rule = kmalloc(sizeof(struct setuid_rule), GFP_KERNEL); + 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 (_setuid_policy_lookup(pol, rule->src_uid, rule->dst_uid) == - SIDPOL_ALLOWED) { + 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; @@ -178,21 +208,31 @@ out_free_rule: * 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); - pol = rcu_replace_pointer(safesetid_setuid_rules, pol, - lockdep_is_held(&policy_update_lock)); - mutex_unlock(&policy_update_lock); + 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); + release_ruleset(pol); return err; } -static ssize_t safesetid_file_write(struct file *file, +static ssize_t safesetid_uid_file_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) @@ -203,38 +243,74 @@ static ssize_t safesetid_file_write(struct file *file, if (*ppos != 0) return -EINVAL; - return handle_policy_update(file, buf, len); + 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) + size_t len, loff_t *ppos, struct mutex *policy_update_lock, struct setid_ruleset* ruleset) { ssize_t res = 0; - struct setuid_ruleset *pol; + struct setid_ruleset *pol; const char *kbuf; - mutex_lock(&policy_update_lock); - pol = rcu_dereference_protected(safesetid_setuid_rules, - lockdep_is_held(&policy_update_lock)); + 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); + mutex_unlock(policy_update_lock); + return res; } -static const struct file_operations safesetid_file_fops = { - .read = safesetid_file_read, - .write = safesetid_file_write, +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 *policy_file; + struct dentry *uid_policy_file; + struct dentry *gid_policy_file; if (!safesetid_initialized) return 0; @@ -245,13 +321,21 @@ static int __init safesetid_init_securityfs(void) 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); + 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: |