Commit | Line | Data |
---|---|---|
aeca4e2c MM |
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 | */ | |
03638e62 JH |
14 | |
15 | #define pr_fmt(fmt) "SafeSetID: " fmt | |
16 | ||
aeca4e2c MM |
17 | #include <linux/security.h> |
18 | #include <linux/cred.h> | |
19 | ||
20 | #include "lsm.h" | |
21 | ||
5294bac9 TC |
22 | static DEFINE_MUTEX(uid_policy_update_lock); |
23 | static DEFINE_MUTEX(gid_policy_update_lock); | |
aeca4e2c MM |
24 | |
25 | /* | |
5294bac9 | 26 | * In the case the input buffer contains one or more invalid IDs, the kid_t |
78ae7df9 | 27 | * variables pointed to by @parent and @child will get updated but this |
aeca4e2c | 28 | * function will return an error. |
78ae7df9 | 29 | * Contents of @buf may be modified. |
aeca4e2c | 30 | */ |
03638e62 | 31 | static int parse_policy_line(struct file *file, char *buf, |
5294bac9 | 32 | struct setid_rule *rule) |
aeca4e2c | 33 | { |
78ae7df9 | 34 | char *child_str; |
aeca4e2c | 35 | int ret; |
78ae7df9 | 36 | u32 parsed_parent, parsed_child; |
aeca4e2c | 37 | |
5294bac9 | 38 | /* Format of |buf| string should be <UID>:<UID> or <GID>:<GID> */ |
78ae7df9 JH |
39 | child_str = strchr(buf, ':'); |
40 | if (child_str == NULL) | |
41 | return -EINVAL; | |
42 | *child_str = '\0'; | |
43 | child_str++; | |
aeca4e2c | 44 | |
78ae7df9 | 45 | ret = kstrtou32(buf, 0, &parsed_parent); |
aeca4e2c | 46 | if (ret) |
78ae7df9 | 47 | return ret; |
aeca4e2c | 48 | |
78ae7df9 | 49 | ret = kstrtou32(child_str, 0, &parsed_child); |
aeca4e2c | 50 | if (ret) |
78ae7df9 | 51 | return ret; |
aeca4e2c | 52 | |
5294bac9 TC |
53 | if (rule->type == UID){ |
54 | rule->src_id.uid = make_kuid(file->f_cred->user_ns, parsed_parent); | |
55 | rule->dst_id.uid = make_kuid(file->f_cred->user_ns, parsed_child); | |
56 | if (!uid_valid(rule->src_id.uid) || !uid_valid(rule->dst_id.uid)) | |
57 | return -EINVAL; | |
58 | } else if (rule->type == GID){ | |
59 | rule->src_id.gid = make_kgid(file->f_cred->user_ns, parsed_parent); | |
60 | rule->dst_id.gid = make_kgid(file->f_cred->user_ns, parsed_child); | |
61 | if (!gid_valid(rule->src_id.gid) || !gid_valid(rule->dst_id.gid)) | |
62 | return -EINVAL; | |
63 | } else { | |
64 | /* Error, rule->type is an invalid type */ | |
78ae7df9 | 65 | return -EINVAL; |
5294bac9 | 66 | } |
78ae7df9 JH |
67 | return 0; |
68 | } | |
69 | ||
03638e62 | 70 | static void __release_ruleset(struct rcu_head *rcu) |
78ae7df9 | 71 | { |
5294bac9 TC |
72 | struct setid_ruleset *pol = |
73 | container_of(rcu, struct setid_ruleset, rcu); | |
03638e62 | 74 | int bucket; |
5294bac9 | 75 | struct setid_rule *rule; |
03638e62 JH |
76 | struct hlist_node *tmp; |
77 | ||
78 | hash_for_each_safe(pol->rules, bucket, tmp, rule, next) | |
79 | kfree(rule); | |
fbd9acb2 | 80 | kfree(pol->policy_str); |
03638e62 JH |
81 | kfree(pol); |
82 | } | |
78ae7df9 | 83 | |
5294bac9 | 84 | static void release_ruleset(struct setid_ruleset *pol){ |
03638e62 JH |
85 | call_rcu(&pol->rcu, __release_ruleset); |
86 | } | |
87 | ||
5294bac9 | 88 | static void insert_rule(struct setid_ruleset *pol, struct setid_rule *rule) |
4f72123d | 89 | { |
5294bac9 TC |
90 | if (pol->type == UID) |
91 | hash_add(pol->rules, &rule->next, __kuid_val(rule->src_id.uid)); | |
92 | else if (pol->type == GID) | |
93 | hash_add(pol->rules, &rule->next, __kgid_val(rule->src_id.gid)); | |
94 | else /* Error, pol->type is neither UID or GID */ | |
95 | return; | |
4f72123d JH |
96 | } |
97 | ||
5294bac9 | 98 | static int verify_ruleset(struct setid_ruleset *pol) |
4f72123d JH |
99 | { |
100 | int bucket; | |
5294bac9 | 101 | struct setid_rule *rule, *nrule; |
4f72123d JH |
102 | int res = 0; |
103 | ||
104 | hash_for_each(pol->rules, bucket, rule, next) { | |
5294bac9 TC |
105 | if (_setid_policy_lookup(pol, rule->dst_id, INVALID_ID) == SIDPOL_DEFAULT) { |
106 | if (pol->type == UID) { | |
107 | pr_warn("insecure policy detected: uid %d is constrained but transitively unconstrained through uid %d\n", | |
108 | __kuid_val(rule->src_id.uid), | |
109 | __kuid_val(rule->dst_id.uid)); | |
110 | } else if (pol->type == GID) { | |
111 | pr_warn("insecure policy detected: gid %d is constrained but transitively unconstrained through gid %d\n", | |
112 | __kgid_val(rule->src_id.gid), | |
113 | __kgid_val(rule->dst_id.gid)); | |
114 | } else { /* pol->type is an invalid type */ | |
115 | res = -EINVAL; | |
116 | return res; | |
117 | } | |
4f72123d JH |
118 | res = -EINVAL; |
119 | ||
120 | /* fix it up */ | |
5294bac9 | 121 | nrule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL); |
4f72123d JH |
122 | if (!nrule) |
123 | return -ENOMEM; | |
5294bac9 TC |
124 | if (pol->type == UID){ |
125 | nrule->src_id.uid = rule->dst_id.uid; | |
126 | nrule->dst_id.uid = rule->dst_id.uid; | |
127 | nrule->type = UID; | |
128 | } else { /* pol->type must be GID if we've made it to here */ | |
129 | nrule->src_id.gid = rule->dst_id.gid; | |
130 | nrule->dst_id.gid = rule->dst_id.gid; | |
131 | nrule->type = GID; | |
132 | } | |
4f72123d JH |
133 | insert_rule(pol, nrule); |
134 | } | |
135 | } | |
136 | return res; | |
137 | } | |
138 | ||
03638e62 | 139 | static ssize_t handle_policy_update(struct file *file, |
5294bac9 | 140 | const char __user *ubuf, size_t len, enum setid_type policy_type) |
03638e62 | 141 | { |
5294bac9 | 142 | struct setid_ruleset *pol; |
03638e62 JH |
143 | char *buf, *p, *end; |
144 | int err; | |
145 | ||
5294bac9 | 146 | pol = kmalloc(sizeof(struct setid_ruleset), GFP_KERNEL); |
03638e62 JH |
147 | if (!pol) |
148 | return -ENOMEM; | |
fbd9acb2 | 149 | pol->policy_str = NULL; |
5294bac9 | 150 | pol->type = policy_type; |
03638e62 JH |
151 | hash_init(pol->rules); |
152 | ||
153 | p = buf = memdup_user_nul(ubuf, len); | |
154 | if (IS_ERR(buf)) { | |
155 | err = PTR_ERR(buf); | |
156 | goto out_free_pol; | |
157 | } | |
fbd9acb2 JH |
158 | pol->policy_str = kstrdup(buf, GFP_KERNEL); |
159 | if (pol->policy_str == NULL) { | |
160 | err = -ENOMEM; | |
161 | goto out_free_buf; | |
162 | } | |
03638e62 JH |
163 | |
164 | /* policy lines, including the last one, end with \n */ | |
165 | while (*p != '\0') { | |
5294bac9 | 166 | struct setid_rule *rule; |
03638e62 JH |
167 | |
168 | end = strchr(p, '\n'); | |
169 | if (end == NULL) { | |
170 | err = -EINVAL; | |
171 | goto out_free_buf; | |
172 | } | |
173 | *end = '\0'; | |
174 | ||
5294bac9 | 175 | rule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL); |
03638e62 JH |
176 | if (!rule) { |
177 | err = -ENOMEM; | |
178 | goto out_free_buf; | |
179 | } | |
180 | ||
5294bac9 | 181 | rule->type = policy_type; |
03638e62 JH |
182 | err = parse_policy_line(file, p, rule); |
183 | if (err) | |
184 | goto out_free_rule; | |
185 | ||
5294bac9 | 186 | if (_setid_policy_lookup(pol, rule->src_id, rule->dst_id) == SIDPOL_ALLOWED) { |
03638e62 JH |
187 | pr_warn("bad policy: duplicate entry\n"); |
188 | err = -EEXIST; | |
189 | goto out_free_rule; | |
190 | } | |
191 | ||
4f72123d | 192 | insert_rule(pol, rule); |
03638e62 JH |
193 | p = end + 1; |
194 | continue; | |
195 | ||
196 | out_free_rule: | |
197 | kfree(rule); | |
198 | goto out_free_buf; | |
199 | } | |
200 | ||
4f72123d JH |
201 | err = verify_ruleset(pol); |
202 | /* bogus policy falls through after fixing it up */ | |
203 | if (err && err != -EINVAL) | |
204 | goto out_free_buf; | |
205 | ||
03638e62 JH |
206 | /* |
207 | * Everything looks good, apply the policy and release the old one. | |
208 | * What we really want here is an xchg() wrapper for RCU, but since that | |
209 | * doesn't currently exist, just use a spinlock for now. | |
210 | */ | |
5294bac9 TC |
211 | if (policy_type == UID) { |
212 | mutex_lock(&uid_policy_update_lock); | |
213 | pol = rcu_replace_pointer(safesetid_setuid_rules, pol, | |
214 | lockdep_is_held(&uid_policy_update_lock)); | |
215 | mutex_unlock(&uid_policy_update_lock); | |
216 | } else if (policy_type == GID) { | |
217 | mutex_lock(&gid_policy_update_lock); | |
218 | pol = rcu_replace_pointer(safesetid_setgid_rules, pol, | |
219 | lockdep_is_held(&gid_policy_update_lock)); | |
220 | mutex_unlock(&gid_policy_update_lock); | |
221 | } else { | |
222 | /* Error, policy type is neither UID or GID */ | |
223 | pr_warn("error: bad policy type"); | |
224 | } | |
03638e62 JH |
225 | err = len; |
226 | ||
227 | out_free_buf: | |
228 | kfree(buf); | |
229 | out_free_pol: | |
21ab8580 | 230 | if (pol) |
5294bac9 | 231 | release_ruleset(pol); |
03638e62 | 232 | return err; |
aeca4e2c MM |
233 | } |
234 | ||
5294bac9 | 235 | static ssize_t safesetid_uid_file_write(struct file *file, |
aeca4e2c MM |
236 | const char __user *buf, |
237 | size_t len, | |
238 | loff_t *ppos) | |
239 | { | |
71a98971 | 240 | if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) |
aeca4e2c MM |
241 | return -EPERM; |
242 | ||
243 | if (*ppos != 0) | |
244 | return -EINVAL; | |
245 | ||
5294bac9 TC |
246 | return handle_policy_update(file, buf, len, UID); |
247 | } | |
248 | ||
249 | static ssize_t safesetid_gid_file_write(struct file *file, | |
250 | const char __user *buf, | |
251 | size_t len, | |
252 | loff_t *ppos) | |
253 | { | |
254 | if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) | |
255 | return -EPERM; | |
256 | ||
257 | if (*ppos != 0) | |
258 | return -EINVAL; | |
259 | ||
260 | return handle_policy_update(file, buf, len, GID); | |
aeca4e2c MM |
261 | } |
262 | ||
fbd9acb2 | 263 | static ssize_t safesetid_file_read(struct file *file, char __user *buf, |
03ca0ec1 | 264 | size_t len, loff_t *ppos, struct mutex *policy_update_lock, struct __rcu setid_ruleset* ruleset) |
fbd9acb2 JH |
265 | { |
266 | ssize_t res = 0; | |
5294bac9 | 267 | struct setid_ruleset *pol; |
fbd9acb2 JH |
268 | const char *kbuf; |
269 | ||
5294bac9 TC |
270 | mutex_lock(policy_update_lock); |
271 | pol = rcu_dereference_protected(ruleset, lockdep_is_held(policy_update_lock)); | |
fbd9acb2 JH |
272 | if (pol) { |
273 | kbuf = pol->policy_str; | |
274 | res = simple_read_from_buffer(buf, len, ppos, | |
275 | kbuf, strlen(kbuf)); | |
276 | } | |
5294bac9 TC |
277 | mutex_unlock(policy_update_lock); |
278 | ||
fbd9acb2 JH |
279 | return res; |
280 | } | |
281 | ||
5294bac9 TC |
282 | static ssize_t safesetid_uid_file_read(struct file *file, char __user *buf, |
283 | size_t len, loff_t *ppos) | |
284 | { | |
285 | return safesetid_file_read(file, buf, len, ppos, | |
286 | &uid_policy_update_lock, safesetid_setuid_rules); | |
287 | } | |
288 | ||
289 | static ssize_t safesetid_gid_file_read(struct file *file, char __user *buf, | |
290 | size_t len, loff_t *ppos) | |
291 | { | |
292 | return safesetid_file_read(file, buf, len, ppos, | |
293 | &gid_policy_update_lock, safesetid_setgid_rules); | |
294 | } | |
295 | ||
296 | ||
297 | ||
298 | static const struct file_operations safesetid_uid_file_fops = { | |
299 | .read = safesetid_uid_file_read, | |
300 | .write = safesetid_uid_file_write, | |
301 | }; | |
302 | ||
303 | static const struct file_operations safesetid_gid_file_fops = { | |
304 | .read = safesetid_gid_file_read, | |
305 | .write = safesetid_gid_file_write, | |
aeca4e2c MM |
306 | }; |
307 | ||
aeca4e2c MM |
308 | static int __init safesetid_init_securityfs(void) |
309 | { | |
aeca4e2c | 310 | int ret; |
03638e62 | 311 | struct dentry *policy_dir; |
5294bac9 TC |
312 | struct dentry *uid_policy_file; |
313 | struct dentry *gid_policy_file; | |
aeca4e2c MM |
314 | |
315 | if (!safesetid_initialized) | |
316 | return 0; | |
317 | ||
03638e62 JH |
318 | policy_dir = securityfs_create_dir("safesetid", NULL); |
319 | if (IS_ERR(policy_dir)) { | |
320 | ret = PTR_ERR(policy_dir); | |
aeca4e2c MM |
321 | goto error; |
322 | } | |
323 | ||
5294bac9 TC |
324 | uid_policy_file = securityfs_create_file("uid_allowlist_policy", 0600, |
325 | policy_dir, NULL, &safesetid_uid_file_fops); | |
326 | if (IS_ERR(uid_policy_file)) { | |
327 | ret = PTR_ERR(uid_policy_file); | |
03638e62 | 328 | goto error; |
aeca4e2c MM |
329 | } |
330 | ||
5294bac9 TC |
331 | gid_policy_file = securityfs_create_file("gid_allowlist_policy", 0600, |
332 | policy_dir, NULL, &safesetid_gid_file_fops); | |
333 | if (IS_ERR(gid_policy_file)) { | |
334 | ret = PTR_ERR(gid_policy_file); | |
335 | goto error; | |
336 | } | |
337 | ||
338 | ||
aeca4e2c MM |
339 | return 0; |
340 | ||
341 | error: | |
03638e62 | 342 | securityfs_remove(policy_dir); |
aeca4e2c MM |
343 | return ret; |
344 | } | |
345 | fs_initcall(safesetid_init_securityfs); |