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 | ||
fbd9acb2 | 22 | static DEFINE_MUTEX(policy_update_lock); |
aeca4e2c MM |
23 | |
24 | /* | |
25 | * In the case the input buffer contains one or more invalid UIDs, the kuid_t | |
78ae7df9 | 26 | * variables pointed to by @parent and @child will get updated but this |
aeca4e2c | 27 | * function will return an error. |
78ae7df9 | 28 | * Contents of @buf may be modified. |
aeca4e2c | 29 | */ |
03638e62 JH |
30 | static int parse_policy_line(struct file *file, char *buf, |
31 | struct setuid_rule *rule) | |
aeca4e2c | 32 | { |
78ae7df9 | 33 | char *child_str; |
aeca4e2c | 34 | int ret; |
78ae7df9 | 35 | u32 parsed_parent, parsed_child; |
aeca4e2c | 36 | |
78ae7df9 JH |
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++; | |
aeca4e2c | 43 | |
78ae7df9 | 44 | ret = kstrtou32(buf, 0, &parsed_parent); |
aeca4e2c | 45 | if (ret) |
78ae7df9 | 46 | return ret; |
aeca4e2c | 47 | |
78ae7df9 | 48 | ret = kstrtou32(child_str, 0, &parsed_child); |
aeca4e2c | 49 | if (ret) |
78ae7df9 | 50 | return ret; |
aeca4e2c | 51 | |
03638e62 JH |
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)) | |
78ae7df9 | 55 | return -EINVAL; |
aeca4e2c | 56 | |
78ae7df9 JH |
57 | return 0; |
58 | } | |
59 | ||
03638e62 | 60 | static void __release_ruleset(struct rcu_head *rcu) |
78ae7df9 | 61 | { |
03638e62 JH |
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; | |
67 | ||
68 | hash_for_each_safe(pol->rules, bucket, tmp, rule, next) | |
69 | kfree(rule); | |
fbd9acb2 | 70 | kfree(pol->policy_str); |
03638e62 JH |
71 | kfree(pol); |
72 | } | |
78ae7df9 | 73 | |
03638e62 JH |
74 | static void release_ruleset(struct setuid_ruleset *pol) |
75 | { | |
76 | call_rcu(&pol->rcu, __release_ruleset); | |
77 | } | |
78 | ||
4f72123d JH |
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 | ||
03638e62 JH |
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; | |
fbd9acb2 | 120 | pol->policy_str = NULL; |
03638e62 JH |
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 | } | |
fbd9acb2 JH |
128 | pol->policy_str = kstrdup(buf, GFP_KERNEL); |
129 | if (pol->policy_str == NULL) { | |
130 | err = -ENOMEM; | |
131 | goto out_free_buf; | |
132 | } | |
03638e62 JH |
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 | ||
4f72123d | 162 | insert_rule(pol, rule); |
03638e62 JH |
163 | p = end + 1; |
164 | continue; | |
165 | ||
166 | out_free_rule: | |
167 | kfree(rule); | |
168 | goto out_free_buf; | |
169 | } | |
170 | ||
4f72123d JH |
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 | ||
03638e62 JH |
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 | */ | |
fbd9acb2 | 181 | mutex_lock(&policy_update_lock); |
a60a5746 PM |
182 | pol = rcu_replace_pointer(safesetid_setuid_rules, pol, |
183 | lockdep_is_held(&policy_update_lock)); | |
fbd9acb2 | 184 | mutex_unlock(&policy_update_lock); |
03638e62 JH |
185 | err = len; |
186 | ||
187 | out_free_buf: | |
188 | kfree(buf); | |
189 | out_free_pol: | |
21ab8580 MM |
190 | if (pol) |
191 | release_ruleset(pol); | |
03638e62 | 192 | return err; |
aeca4e2c MM |
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 | { | |
71a98971 | 200 | if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) |
aeca4e2c MM |
201 | return -EPERM; |
202 | ||
203 | if (*ppos != 0) | |
204 | return -EINVAL; | |
205 | ||
03638e62 | 206 | return handle_policy_update(file, buf, len); |
aeca4e2c MM |
207 | } |
208 | ||
fbd9acb2 JH |
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 | ||
aeca4e2c | 228 | static const struct file_operations safesetid_file_fops = { |
fbd9acb2 | 229 | .read = safesetid_file_read, |
aeca4e2c MM |
230 | .write = safesetid_file_write, |
231 | }; | |
232 | ||
aeca4e2c MM |
233 | static int __init safesetid_init_securityfs(void) |
234 | { | |
aeca4e2c | 235 | int ret; |
03638e62 JH |
236 | struct dentry *policy_dir; |
237 | struct dentry *policy_file; | |
aeca4e2c MM |
238 | |
239 | if (!safesetid_initialized) | |
240 | return 0; | |
241 | ||
03638e62 JH |
242 | policy_dir = securityfs_create_dir("safesetid", NULL); |
243 | if (IS_ERR(policy_dir)) { | |
244 | ret = PTR_ERR(policy_dir); | |
aeca4e2c MM |
245 | goto error; |
246 | } | |
247 | ||
fbd9acb2 | 248 | policy_file = securityfs_create_file("whitelist_policy", 0600, |
03638e62 JH |
249 | policy_dir, NULL, &safesetid_file_fops); |
250 | if (IS_ERR(policy_file)) { | |
251 | ret = PTR_ERR(policy_file); | |
252 | goto error; | |
aeca4e2c MM |
253 | } |
254 | ||
255 | return 0; | |
256 | ||
257 | error: | |
03638e62 | 258 | securityfs_remove(policy_dir); |
aeca4e2c MM |
259 | return ret; |
260 | } | |
261 | fs_initcall(safesetid_init_securityfs); |