Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 1 | // SPDX-License-Identifier: GPL-2.0 |
| 2 | /* |
| 3 | * SafeSetID Linux Security Module |
| 4 | * |
| 5 | * Author: Micah Morton <mortonm@chromium.org> |
| 6 | * |
| 7 | * Copyright (C) 2018 The Chromium OS Authors. |
| 8 | * |
| 9 | * This program is free software; you can redistribute it and/or modify |
| 10 | * it under the terms of the GNU General Public License version 2, as |
| 11 | * published by the Free Software Foundation. |
| 12 | * |
| 13 | */ |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 14 | |
| 15 | #define pr_fmt(fmt) "SafeSetID: " fmt |
| 16 | |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 17 | #include <linux/security.h> |
| 18 | #include <linux/cred.h> |
| 19 | |
| 20 | #include "lsm.h" |
| 21 | |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 22 | static DEFINE_MUTEX(policy_update_lock); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 23 | |
| 24 | /* |
| 25 | * In the case the input buffer contains one or more invalid UIDs, the kuid_t |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 26 | * variables pointed to by @parent and @child will get updated but this |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 27 | * function will return an error. |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 28 | * Contents of @buf may be modified. |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 29 | */ |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 30 | static int parse_policy_line(struct file *file, char *buf, |
| 31 | struct setuid_rule *rule) |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 32 | { |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 33 | char *child_str; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 34 | int ret; |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 35 | u32 parsed_parent, parsed_child; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 36 | |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 37 | /* Format of |buf| string should be <UID>:<UID>. */ |
| 38 | child_str = strchr(buf, ':'); |
| 39 | if (child_str == NULL) |
| 40 | return -EINVAL; |
| 41 | *child_str = '\0'; |
| 42 | child_str++; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 43 | |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 44 | ret = kstrtou32(buf, 0, &parsed_parent); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 45 | if (ret) |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 46 | return ret; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 47 | |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 48 | ret = kstrtou32(child_str, 0, &parsed_child); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 49 | if (ret) |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 50 | return ret; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 51 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 52 | rule->src_uid = make_kuid(file->f_cred->user_ns, parsed_parent); |
| 53 | rule->dst_uid = make_kuid(file->f_cred->user_ns, parsed_child); |
| 54 | if (!uid_valid(rule->src_uid) || !uid_valid(rule->dst_uid)) |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 55 | return -EINVAL; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 56 | |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 57 | return 0; |
| 58 | } |
| 59 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 60 | static void __release_ruleset(struct rcu_head *rcu) |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 61 | { |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 62 | struct setuid_ruleset *pol = |
| 63 | container_of(rcu, struct setuid_ruleset, rcu); |
| 64 | int bucket; |
| 65 | struct setuid_rule *rule; |
| 66 | struct hlist_node *tmp; |
Jann Horn | 78ae7df | 2019-04-10 09:55:48 -0700 | [diff] [blame] | 67 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 68 | hash_for_each_safe(pol->rules, bucket, tmp, rule, next) |
| 69 | kfree(rule); |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 70 | kfree(pol->policy_str); |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 71 | kfree(pol); |
| 72 | } |
| 73 | |
| 74 | static void release_ruleset(struct setuid_ruleset *pol) |
| 75 | { |
| 76 | call_rcu(&pol->rcu, __release_ruleset); |
| 77 | } |
| 78 | |
Jann Horn | 4f72123 | 2019-04-11 13:12:43 -0700 | [diff] [blame] | 79 | static void insert_rule(struct setuid_ruleset *pol, struct setuid_rule *rule) |
| 80 | { |
| 81 | hash_add(pol->rules, &rule->next, __kuid_val(rule->src_uid)); |
| 82 | } |
| 83 | |
| 84 | static int verify_ruleset(struct setuid_ruleset *pol) |
| 85 | { |
| 86 | int bucket; |
| 87 | struct setuid_rule *rule, *nrule; |
| 88 | int res = 0; |
| 89 | |
| 90 | hash_for_each(pol->rules, bucket, rule, next) { |
| 91 | if (_setuid_policy_lookup(pol, rule->dst_uid, INVALID_UID) == |
| 92 | SIDPOL_DEFAULT) { |
| 93 | pr_warn("insecure policy detected: uid %d is constrained but transitively unconstrained through uid %d\n", |
| 94 | __kuid_val(rule->src_uid), |
| 95 | __kuid_val(rule->dst_uid)); |
| 96 | res = -EINVAL; |
| 97 | |
| 98 | /* fix it up */ |
| 99 | nrule = kmalloc(sizeof(struct setuid_rule), GFP_KERNEL); |
| 100 | if (!nrule) |
| 101 | return -ENOMEM; |
| 102 | nrule->src_uid = rule->dst_uid; |
| 103 | nrule->dst_uid = rule->dst_uid; |
| 104 | insert_rule(pol, nrule); |
| 105 | } |
| 106 | } |
| 107 | return res; |
| 108 | } |
| 109 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 110 | static ssize_t handle_policy_update(struct file *file, |
| 111 | const char __user *ubuf, size_t len) |
| 112 | { |
| 113 | struct setuid_ruleset *pol; |
| 114 | char *buf, *p, *end; |
| 115 | int err; |
| 116 | |
| 117 | pol = kmalloc(sizeof(struct setuid_ruleset), GFP_KERNEL); |
| 118 | if (!pol) |
| 119 | return -ENOMEM; |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 120 | pol->policy_str = NULL; |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 121 | hash_init(pol->rules); |
| 122 | |
| 123 | p = buf = memdup_user_nul(ubuf, len); |
| 124 | if (IS_ERR(buf)) { |
| 125 | err = PTR_ERR(buf); |
| 126 | goto out_free_pol; |
| 127 | } |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 128 | pol->policy_str = kstrdup(buf, GFP_KERNEL); |
| 129 | if (pol->policy_str == NULL) { |
| 130 | err = -ENOMEM; |
| 131 | goto out_free_buf; |
| 132 | } |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 133 | |
| 134 | /* policy lines, including the last one, end with \n */ |
| 135 | while (*p != '\0') { |
| 136 | struct setuid_rule *rule; |
| 137 | |
| 138 | end = strchr(p, '\n'); |
| 139 | if (end == NULL) { |
| 140 | err = -EINVAL; |
| 141 | goto out_free_buf; |
| 142 | } |
| 143 | *end = '\0'; |
| 144 | |
| 145 | rule = kmalloc(sizeof(struct setuid_rule), GFP_KERNEL); |
| 146 | if (!rule) { |
| 147 | err = -ENOMEM; |
| 148 | goto out_free_buf; |
| 149 | } |
| 150 | |
| 151 | err = parse_policy_line(file, p, rule); |
| 152 | if (err) |
| 153 | goto out_free_rule; |
| 154 | |
| 155 | if (_setuid_policy_lookup(pol, rule->src_uid, rule->dst_uid) == |
| 156 | SIDPOL_ALLOWED) { |
| 157 | pr_warn("bad policy: duplicate entry\n"); |
| 158 | err = -EEXIST; |
| 159 | goto out_free_rule; |
| 160 | } |
| 161 | |
Jann Horn | 4f72123 | 2019-04-11 13:12:43 -0700 | [diff] [blame] | 162 | insert_rule(pol, rule); |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 163 | p = end + 1; |
| 164 | continue; |
| 165 | |
| 166 | out_free_rule: |
| 167 | kfree(rule); |
| 168 | goto out_free_buf; |
| 169 | } |
| 170 | |
Jann Horn | 4f72123 | 2019-04-11 13:12:43 -0700 | [diff] [blame] | 171 | err = verify_ruleset(pol); |
| 172 | /* bogus policy falls through after fixing it up */ |
| 173 | if (err && err != -EINVAL) |
| 174 | goto out_free_buf; |
| 175 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 176 | /* |
| 177 | * Everything looks good, apply the policy and release the old one. |
| 178 | * What we really want here is an xchg() wrapper for RCU, but since that |
| 179 | * doesn't currently exist, just use a spinlock for now. |
| 180 | */ |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 181 | mutex_lock(&policy_update_lock); |
Paul E. McKenney | a60a574 | 2019-10-04 15:07:09 -0700 | [diff] [blame] | 182 | pol = rcu_replace_pointer(safesetid_setuid_rules, pol, |
| 183 | lockdep_is_held(&policy_update_lock)); |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 184 | mutex_unlock(&policy_update_lock); |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 185 | err = len; |
| 186 | |
| 187 | out_free_buf: |
| 188 | kfree(buf); |
| 189 | out_free_pol: |
Micah Morton | 21ab858 | 2019-09-17 11:27:05 -0700 | [diff] [blame] | 190 | if (pol) |
| 191 | release_ruleset(pol); |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 192 | return err; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 193 | } |
| 194 | |
| 195 | static ssize_t safesetid_file_write(struct file *file, |
| 196 | const char __user *buf, |
| 197 | size_t len, |
| 198 | loff_t *ppos) |
| 199 | { |
Jann Horn | 71a9897 | 2019-04-10 09:55:58 -0700 | [diff] [blame] | 200 | if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 201 | return -EPERM; |
| 202 | |
| 203 | if (*ppos != 0) |
| 204 | return -EINVAL; |
| 205 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 206 | return handle_policy_update(file, buf, len); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 207 | } |
| 208 | |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 209 | static ssize_t safesetid_file_read(struct file *file, char __user *buf, |
| 210 | size_t len, loff_t *ppos) |
| 211 | { |
| 212 | ssize_t res = 0; |
| 213 | struct setuid_ruleset *pol; |
| 214 | const char *kbuf; |
| 215 | |
| 216 | mutex_lock(&policy_update_lock); |
| 217 | pol = rcu_dereference_protected(safesetid_setuid_rules, |
| 218 | lockdep_is_held(&policy_update_lock)); |
| 219 | if (pol) { |
| 220 | kbuf = pol->policy_str; |
| 221 | res = simple_read_from_buffer(buf, len, ppos, |
| 222 | kbuf, strlen(kbuf)); |
| 223 | } |
| 224 | mutex_unlock(&policy_update_lock); |
| 225 | return res; |
| 226 | } |
| 227 | |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 228 | static const struct file_operations safesetid_file_fops = { |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 229 | .read = safesetid_file_read, |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 230 | .write = safesetid_file_write, |
| 231 | }; |
| 232 | |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 233 | static int __init safesetid_init_securityfs(void) |
| 234 | { |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 235 | int ret; |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 236 | struct dentry *policy_dir; |
| 237 | struct dentry *policy_file; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 238 | |
| 239 | if (!safesetid_initialized) |
| 240 | return 0; |
| 241 | |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 242 | policy_dir = securityfs_create_dir("safesetid", NULL); |
| 243 | if (IS_ERR(policy_dir)) { |
| 244 | ret = PTR_ERR(policy_dir); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 245 | goto error; |
| 246 | } |
| 247 | |
Jann Horn | fbd9acb | 2019-04-11 13:11:54 -0700 | [diff] [blame] | 248 | policy_file = securityfs_create_file("whitelist_policy", 0600, |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 249 | policy_dir, NULL, &safesetid_file_fops); |
| 250 | if (IS_ERR(policy_file)) { |
| 251 | ret = PTR_ERR(policy_file); |
| 252 | goto error; |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 253 | } |
| 254 | |
| 255 | return 0; |
| 256 | |
| 257 | error: |
Jann Horn | 03638e6 | 2019-04-10 09:56:05 -0700 | [diff] [blame] | 258 | securityfs_remove(policy_dir); |
Micah Morton | aeca4e2 | 2019-01-16 07:46:06 -0800 | [diff] [blame] | 259 | return ret; |
| 260 | } |
| 261 | fs_initcall(safesetid_init_securityfs); |