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 | */ |
14 | |
15 | #define pr_fmt(fmt) "SafeSetID: " fmt |
16 | |
17 | #include <linux/security.h> |
18 | #include <linux/cred.h> |
19 | |
20 | #include "lsm.h" |
21 | |
22 | static DEFINE_MUTEX(uid_policy_update_lock); |
23 | static DEFINE_MUTEX(gid_policy_update_lock); |
24 | |
25 | /* |
26 | * In the case the input buffer contains one or more invalid IDs, the kid_t |
27 | * variables pointed to by @parent and @child will get updated but this |
28 | * function will return an error. |
29 | * Contents of @buf may be modified. |
30 | */ |
31 | static int parse_policy_line(struct file *file, char *buf, |
32 | struct setid_rule *rule) |
33 | { |
34 | char *child_str; |
35 | int ret; |
36 | u32 parsed_parent, parsed_child; |
37 | |
38 | /* Format of |buf| string should be <UID>:<UID> or <GID>:<GID> */ |
39 | child_str = strchr(buf, ':'); |
40 | if (child_str == NULL) |
41 | return -EINVAL; |
42 | *child_str = '\0'; |
43 | child_str++; |
44 | |
45 | ret = kstrtou32(s: buf, base: 0, res: &parsed_parent); |
46 | if (ret) |
47 | return ret; |
48 | |
49 | ret = kstrtou32(s: child_str, base: 0, res: &parsed_child); |
50 | if (ret) |
51 | return ret; |
52 | |
53 | if (rule->type == UID){ |
54 | rule->src_id.uid = make_kuid(from: file->f_cred->user_ns, uid: parsed_parent); |
55 | rule->dst_id.uid = make_kuid(from: file->f_cred->user_ns, uid: parsed_child); |
56 | if (!uid_valid(uid: rule->src_id.uid) || !uid_valid(uid: rule->dst_id.uid)) |
57 | return -EINVAL; |
58 | } else if (rule->type == GID){ |
59 | rule->src_id.gid = make_kgid(from: file->f_cred->user_ns, gid: parsed_parent); |
60 | rule->dst_id.gid = make_kgid(from: file->f_cred->user_ns, gid: parsed_child); |
61 | if (!gid_valid(gid: rule->src_id.gid) || !gid_valid(gid: rule->dst_id.gid)) |
62 | return -EINVAL; |
63 | } else { |
64 | /* Error, rule->type is an invalid type */ |
65 | return -EINVAL; |
66 | } |
67 | return 0; |
68 | } |
69 | |
70 | static void __release_ruleset(struct rcu_head *rcu) |
71 | { |
72 | struct setid_ruleset *pol = |
73 | container_of(rcu, struct setid_ruleset, rcu); |
74 | int bucket; |
75 | struct setid_rule *rule; |
76 | struct hlist_node *tmp; |
77 | |
78 | hash_for_each_safe(pol->rules, bucket, tmp, rule, next) |
79 | kfree(objp: rule); |
80 | kfree(objp: pol->policy_str); |
81 | kfree(objp: pol); |
82 | } |
83 | |
84 | static void release_ruleset(struct setid_ruleset *pol){ |
85 | call_rcu(head: &pol->rcu, func: __release_ruleset); |
86 | } |
87 | |
88 | static void insert_rule(struct setid_ruleset *pol, struct setid_rule *rule) |
89 | { |
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; |
96 | } |
97 | |
98 | static int verify_ruleset(struct setid_ruleset *pol) |
99 | { |
100 | int bucket; |
101 | struct setid_rule *rule, *nrule; |
102 | int res = 0; |
103 | |
104 | hash_for_each(pol->rules, bucket, rule, next) { |
105 | if (_setid_policy_lookup(policy: pol, src: 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 | } |
118 | res = -EINVAL; |
119 | |
120 | /* fix it up */ |
121 | nrule = kmalloc(size: sizeof(struct setid_rule), GFP_KERNEL); |
122 | if (!nrule) |
123 | return -ENOMEM; |
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 | } |
133 | insert_rule(pol, rule: nrule); |
134 | } |
135 | } |
136 | return res; |
137 | } |
138 | |
139 | static ssize_t handle_policy_update(struct file *file, |
140 | const char __user *ubuf, size_t len, enum setid_type policy_type) |
141 | { |
142 | struct setid_ruleset *pol; |
143 | char *buf, *p, *end; |
144 | int err; |
145 | |
146 | pol = kmalloc(size: sizeof(struct setid_ruleset), GFP_KERNEL); |
147 | if (!pol) |
148 | return -ENOMEM; |
149 | pol->policy_str = NULL; |
150 | pol->type = policy_type; |
151 | hash_init(pol->rules); |
152 | |
153 | p = buf = memdup_user_nul(ubuf, len); |
154 | if (IS_ERR(ptr: buf)) { |
155 | err = PTR_ERR(ptr: buf); |
156 | goto out_free_pol; |
157 | } |
158 | pol->policy_str = kstrdup(s: buf, GFP_KERNEL); |
159 | if (pol->policy_str == NULL) { |
160 | err = -ENOMEM; |
161 | goto out_free_buf; |
162 | } |
163 | |
164 | /* policy lines, including the last one, end with \n */ |
165 | while (*p != '\0') { |
166 | struct setid_rule *rule; |
167 | |
168 | end = strchr(p, '\n'); |
169 | if (end == NULL) { |
170 | err = -EINVAL; |
171 | goto out_free_buf; |
172 | } |
173 | *end = '\0'; |
174 | |
175 | rule = kmalloc(size: sizeof(struct setid_rule), GFP_KERNEL); |
176 | if (!rule) { |
177 | err = -ENOMEM; |
178 | goto out_free_buf; |
179 | } |
180 | |
181 | rule->type = policy_type; |
182 | err = parse_policy_line(file, buf: p, rule); |
183 | if (err) |
184 | goto out_free_rule; |
185 | |
186 | if (_setid_policy_lookup(policy: pol, src: rule->src_id, dst: rule->dst_id) == SIDPOL_ALLOWED) { |
187 | pr_warn("bad policy: duplicate entry\n" ); |
188 | err = -EEXIST; |
189 | goto out_free_rule; |
190 | } |
191 | |
192 | insert_rule(pol, rule); |
193 | p = end + 1; |
194 | continue; |
195 | |
196 | out_free_rule: |
197 | kfree(objp: rule); |
198 | goto out_free_buf; |
199 | } |
200 | |
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 | |
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 | */ |
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(lock: &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(lock: &gid_policy_update_lock); |
221 | } else { |
222 | /* Error, policy type is neither UID or GID */ |
223 | pr_warn("error: bad policy type" ); |
224 | } |
225 | err = len; |
226 | |
227 | out_free_buf: |
228 | kfree(objp: buf); |
229 | out_free_pol: |
230 | if (pol) |
231 | release_ruleset(pol); |
232 | return err; |
233 | } |
234 | |
235 | static ssize_t safesetid_uid_file_write(struct file *file, |
236 | const char __user *buf, |
237 | size_t len, |
238 | loff_t *ppos) |
239 | { |
240 | if (!file_ns_capable(file, ns: &init_user_ns, CAP_MAC_ADMIN)) |
241 | return -EPERM; |
242 | |
243 | if (*ppos != 0) |
244 | return -EINVAL; |
245 | |
246 | return handle_policy_update(file, ubuf: buf, len, policy_type: 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, ns: &init_user_ns, CAP_MAC_ADMIN)) |
255 | return -EPERM; |
256 | |
257 | if (*ppos != 0) |
258 | return -EINVAL; |
259 | |
260 | return handle_policy_update(file, ubuf: buf, len, policy_type: GID); |
261 | } |
262 | |
263 | static ssize_t safesetid_file_read(struct file *file, char __user *buf, |
264 | size_t len, loff_t *ppos, struct mutex *policy_update_lock, struct __rcu setid_ruleset* ruleset) |
265 | { |
266 | ssize_t res = 0; |
267 | struct setid_ruleset *pol; |
268 | const char *kbuf; |
269 | |
270 | mutex_lock(policy_update_lock); |
271 | pol = rcu_dereference_protected(ruleset, lockdep_is_held(policy_update_lock)); |
272 | if (pol) { |
273 | kbuf = pol->policy_str; |
274 | res = simple_read_from_buffer(to: buf, count: len, ppos, |
275 | from: kbuf, strlen(kbuf)); |
276 | } |
277 | mutex_unlock(lock: policy_update_lock); |
278 | |
279 | return res; |
280 | } |
281 | |
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 | policy_update_lock: &uid_policy_update_lock, ruleset: 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 | policy_update_lock: &gid_policy_update_lock, ruleset: 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, |
306 | }; |
307 | |
308 | static int __init safesetid_init_securityfs(void) |
309 | { |
310 | int ret; |
311 | struct dentry *policy_dir; |
312 | struct dentry *uid_policy_file; |
313 | struct dentry *gid_policy_file; |
314 | |
315 | if (!safesetid_initialized) |
316 | return 0; |
317 | |
318 | policy_dir = securityfs_create_dir(name: "safesetid" , NULL); |
319 | if (IS_ERR(ptr: policy_dir)) { |
320 | ret = PTR_ERR(ptr: policy_dir); |
321 | goto error; |
322 | } |
323 | |
324 | uid_policy_file = securityfs_create_file(name: "uid_allowlist_policy" , mode: 0600, |
325 | parent: policy_dir, NULL, fops: &safesetid_uid_file_fops); |
326 | if (IS_ERR(ptr: uid_policy_file)) { |
327 | ret = PTR_ERR(ptr: uid_policy_file); |
328 | goto error; |
329 | } |
330 | |
331 | gid_policy_file = securityfs_create_file(name: "gid_allowlist_policy" , mode: 0600, |
332 | parent: policy_dir, NULL, fops: &safesetid_gid_file_fops); |
333 | if (IS_ERR(ptr: gid_policy_file)) { |
334 | ret = PTR_ERR(ptr: gid_policy_file); |
335 | goto error; |
336 | } |
337 | |
338 | |
339 | return 0; |
340 | |
341 | error: |
342 | securityfs_remove(dentry: policy_dir); |
343 | return ret; |
344 | } |
345 | fs_initcall(safesetid_init_securityfs); |
346 | |