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 | */ | |
14 | ||
15 | #define pr_fmt(fmt) "SafeSetID: " fmt | |
16 | ||
aeca4e2c MM |
17 | #include <linux/lsm_hooks.h> |
18 | #include <linux/module.h> | |
19 | #include <linux/ptrace.h> | |
20 | #include <linux/sched/task_stack.h> | |
21 | #include <linux/security.h> | |
1cd02a27 | 22 | #include "lsm.h" |
aeca4e2c MM |
23 | |
24 | /* Flag indicating whether initialization completed */ | |
25 | int safesetid_initialized; | |
26 | ||
5294bac9 TC |
27 | struct setid_ruleset __rcu *safesetid_setuid_rules; |
28 | struct setid_ruleset __rcu *safesetid_setgid_rules; | |
29 | ||
aeca4e2c | 30 | |
03638e62 | 31 | /* Compute a decision for a transition from @src to @dst under @policy. */ |
5294bac9 TC |
32 | enum sid_policy_type _setid_policy_lookup(struct setid_ruleset *policy, |
33 | kid_t src, kid_t dst) | |
aeca4e2c | 34 | { |
5294bac9 | 35 | struct setid_rule *rule; |
1cd02a27 | 36 | enum sid_policy_type result = SIDPOL_DEFAULT; |
aeca4e2c | 37 | |
5294bac9 TC |
38 | if (policy->type == UID) { |
39 | hash_for_each_possible(policy->rules, rule, next, __kuid_val(src.uid)) { | |
40 | if (!uid_eq(rule->src_id.uid, src.uid)) | |
41 | continue; | |
42 | if (uid_eq(rule->dst_id.uid, dst.uid)) | |
43 | return SIDPOL_ALLOWED; | |
44 | result = SIDPOL_CONSTRAINED; | |
45 | } | |
46 | } else if (policy->type == GID) { | |
47 | hash_for_each_possible(policy->rules, rule, next, __kgid_val(src.gid)) { | |
48 | if (!gid_eq(rule->src_id.gid, src.gid)) | |
49 | continue; | |
50 | if (gid_eq(rule->dst_id.gid, dst.gid)){ | |
51 | return SIDPOL_ALLOWED; | |
52 | } | |
53 | result = SIDPOL_CONSTRAINED; | |
54 | } | |
55 | } else { | |
56 | /* Should not reach here, report the ID as contrainsted */ | |
1cd02a27 | 57 | result = SIDPOL_CONSTRAINED; |
aeca4e2c | 58 | } |
03638e62 JH |
59 | return result; |
60 | } | |
61 | ||
62 | /* | |
63 | * Compute a decision for a transition from @src to @dst under the active | |
64 | * policy. | |
65 | */ | |
5294bac9 | 66 | static enum sid_policy_type setid_policy_lookup(kid_t src, kid_t dst, enum setid_type new_type) |
03638e62 JH |
67 | { |
68 | enum sid_policy_type result = SIDPOL_DEFAULT; | |
5294bac9 | 69 | struct setid_ruleset *pol; |
03638e62 JH |
70 | |
71 | rcu_read_lock(); | |
5294bac9 TC |
72 | if (new_type == UID) |
73 | pol = rcu_dereference(safesetid_setuid_rules); | |
74 | else if (new_type == GID) | |
75 | pol = rcu_dereference(safesetid_setgid_rules); | |
76 | else { /* Should not reach here */ | |
77 | result = SIDPOL_CONSTRAINED; | |
78 | rcu_read_unlock(); | |
79 | return result; | |
80 | } | |
81 | ||
82 | if (pol) { | |
83 | pol->type = new_type; | |
84 | result = _setid_policy_lookup(pol, src, dst); | |
85 | } | |
aeca4e2c | 86 | rcu_read_unlock(); |
1cd02a27 | 87 | return result; |
aeca4e2c MM |
88 | } |
89 | ||
90 | static int safesetid_security_capable(const struct cred *cred, | |
91 | struct user_namespace *ns, | |
92 | int cap, | |
93 | unsigned int opts) | |
94 | { | |
5294bac9 TC |
95 | /* We're only interested in CAP_SETUID and CAP_SETGID. */ |
96 | if (cap != CAP_SETUID && cap != CAP_SETGID) | |
8068866c JH |
97 | return 0; |
98 | ||
99 | /* | |
5294bac9 | 100 | * If CAP_SET{U/G}ID is currently used for a setid() syscall, we want to |
8068866c | 101 | * let it go through here; the real security check happens later, in the |
5294bac9 TC |
102 | * task_fix_set{u/g}id hook. |
103 | * | |
104 | * NOTE: | |
105 | * Until we add support for restricting setgroups() calls, GID security | |
106 | * policies offer no meaningful security since we always return 0 here | |
107 | * when called from within the setgroups() syscall and there is no | |
108 | * additional hook later on to enforce security policies for setgroups(). | |
8068866c JH |
109 | */ |
110 | if ((opts & CAP_OPT_INSETID) != 0) | |
111 | return 0; | |
112 | ||
5294bac9 TC |
113 | switch (cap) { |
114 | case CAP_SETUID: | |
115 | /* | |
116 | * If no policy applies to this task, allow the use of CAP_SETUID for | |
117 | * other purposes. | |
118 | */ | |
03ca0ec1 | 119 | if (setid_policy_lookup((kid_t){.uid = cred->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT) |
5294bac9 TC |
120 | return 0; |
121 | /* | |
122 | * Reject use of CAP_SETUID for functionality other than calling | |
123 | * set*uid() (e.g. setting up userns uid mappings). | |
124 | */ | |
125 | pr_warn("Operation requires CAP_SETUID, which is not available to UID %u for operations besides approved set*uid transitions\n", | |
126 | __kuid_val(cred->uid)); | |
127 | return -EPERM; | |
128 | break; | |
129 | case CAP_SETGID: | |
130 | /* | |
131 | * If no policy applies to this task, allow the use of CAP_SETGID for | |
132 | * other purposes. | |
133 | */ | |
03ca0ec1 | 134 | if (setid_policy_lookup((kid_t){.gid = cred->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT) |
5294bac9 TC |
135 | return 0; |
136 | /* | |
137 | * Reject use of CAP_SETUID for functionality other than calling | |
138 | * set*gid() (e.g. setting up userns gid mappings). | |
139 | */ | |
140 | pr_warn("Operation requires CAP_SETGID, which is not available to GID %u for operations besides approved set*gid transitions\n", | |
141 | __kuid_val(cred->uid)); | |
142 | return -EPERM; | |
143 | break; | |
144 | default: | |
145 | /* Error, the only capabilities were checking for is CAP_SETUID/GID */ | |
8068866c | 146 | return 0; |
5294bac9 TC |
147 | break; |
148 | } | |
149 | return 0; | |
aeca4e2c MM |
150 | } |
151 | ||
7ef6b306 JH |
152 | /* |
153 | * Check whether a caller with old credentials @old is allowed to switch to | |
5294bac9 | 154 | * credentials that contain @new_id. |
7ef6b306 | 155 | */ |
5294bac9 | 156 | static bool id_permitted_for_cred(const struct cred *old, kid_t new_id, enum setid_type new_type) |
aeca4e2c | 157 | { |
7ef6b306 JH |
158 | bool permitted; |
159 | ||
5294bac9 TC |
160 | /* If our old creds already had this ID in it, it's fine. */ |
161 | if (new_type == UID) { | |
162 | if (uid_eq(new_id.uid, old->uid) || uid_eq(new_id.uid, old->euid) || | |
163 | uid_eq(new_id.uid, old->suid)) | |
164 | return true; | |
165 | } else if (new_type == GID){ | |
166 | if (gid_eq(new_id.gid, old->gid) || gid_eq(new_id.gid, old->egid) || | |
167 | gid_eq(new_id.gid, old->sgid)) | |
168 | return true; | |
169 | } else /* Error, new_type is an invalid type */ | |
170 | return false; | |
7ef6b306 | 171 | |
aeca4e2c | 172 | /* |
7ef6b306 JH |
173 | * Transitions to new UIDs require a check against the policy of the old |
174 | * RUID. | |
aeca4e2c | 175 | */ |
1cd02a27 | 176 | permitted = |
03ca0ec1 | 177 | setid_policy_lookup((kid_t){.uid = old->uid}, new_id, new_type) != SIDPOL_CONSTRAINED; |
5294bac9 | 178 | |
7ef6b306 | 179 | if (!permitted) { |
5294bac9 TC |
180 | if (new_type == UID) { |
181 | pr_warn("UID transition ((%d,%d,%d) -> %d) blocked\n", | |
182 | __kuid_val(old->uid), __kuid_val(old->euid), | |
183 | __kuid_val(old->suid), __kuid_val(new_id.uid)); | |
184 | } else if (new_type == GID) { | |
185 | pr_warn("GID transition ((%d,%d,%d) -> %d) blocked\n", | |
186 | __kgid_val(old->gid), __kgid_val(old->egid), | |
187 | __kgid_val(old->sgid), __kgid_val(new_id.gid)); | |
188 | } else /* Error, new_type is an invalid type */ | |
189 | return false; | |
7ef6b306 JH |
190 | } |
191 | return permitted; | |
aeca4e2c MM |
192 | } |
193 | ||
194 | /* | |
195 | * Check whether there is either an exception for user under old cred struct to | |
196 | * set*uid to user under new cred struct, or the UID transition is allowed (by | |
197 | * Linux set*uid rules) even without CAP_SETUID. | |
198 | */ | |
199 | static int safesetid_task_fix_setuid(struct cred *new, | |
200 | const struct cred *old, | |
201 | int flags) | |
202 | { | |
203 | ||
7ef6b306 | 204 | /* Do nothing if there are no setuid restrictions for our old RUID. */ |
03ca0ec1 | 205 | if (setid_policy_lookup((kid_t){.uid = old->uid}, INVALID_ID, UID) == SIDPOL_DEFAULT) |
5294bac9 TC |
206 | return 0; |
207 | ||
03ca0ec1 TC |
208 | if (id_permitted_for_cred(old, (kid_t){.uid = new->uid}, UID) && |
209 | id_permitted_for_cred(old, (kid_t){.uid = new->euid}, UID) && | |
210 | id_permitted_for_cred(old, (kid_t){.uid = new->suid}, UID) && | |
211 | id_permitted_for_cred(old, (kid_t){.uid = new->fsuid}, UID)) | |
5294bac9 TC |
212 | return 0; |
213 | ||
214 | /* | |
215 | * Kill this process to avoid potential security vulnerabilities | |
216 | * that could arise from a missing allowlist entry preventing a | |
217 | * privileged process from dropping to a lesser-privileged one. | |
218 | */ | |
219 | force_sig(SIGKILL); | |
220 | return -EACCES; | |
221 | } | |
222 | ||
223 | static int safesetid_task_fix_setgid(struct cred *new, | |
224 | const struct cred *old, | |
225 | int flags) | |
226 | { | |
227 | ||
228 | /* Do nothing if there are no setgid restrictions for our old RGID. */ | |
03ca0ec1 | 229 | if (setid_policy_lookup((kid_t){.gid = old->gid}, INVALID_ID, GID) == SIDPOL_DEFAULT) |
aeca4e2c MM |
230 | return 0; |
231 | ||
03ca0ec1 TC |
232 | if (id_permitted_for_cred(old, (kid_t){.gid = new->gid}, GID) && |
233 | id_permitted_for_cred(old, (kid_t){.gid = new->egid}, GID) && | |
234 | id_permitted_for_cred(old, (kid_t){.gid = new->sgid}, GID) && | |
235 | id_permitted_for_cred(old, (kid_t){.gid = new->fsgid}, GID)) | |
7ef6b306 JH |
236 | return 0; |
237 | ||
238 | /* | |
239 | * Kill this process to avoid potential security vulnerabilities | |
5294bac9 | 240 | * that could arise from a missing allowlist entry preventing a |
7ef6b306 JH |
241 | * privileged process from dropping to a lesser-privileged one. |
242 | */ | |
243 | force_sig(SIGKILL); | |
244 | return -EACCES; | |
aeca4e2c MM |
245 | } |
246 | ||
aeca4e2c MM |
247 | static struct security_hook_list safesetid_security_hooks[] = { |
248 | LSM_HOOK_INIT(task_fix_setuid, safesetid_task_fix_setuid), | |
5294bac9 | 249 | LSM_HOOK_INIT(task_fix_setgid, safesetid_task_fix_setgid), |
aeca4e2c MM |
250 | LSM_HOOK_INIT(capable, safesetid_security_capable) |
251 | }; | |
252 | ||
253 | static int __init safesetid_security_init(void) | |
254 | { | |
255 | security_add_hooks(safesetid_security_hooks, | |
256 | ARRAY_SIZE(safesetid_security_hooks), "safesetid"); | |
257 | ||
258 | /* Report that SafeSetID successfully initialized */ | |
259 | safesetid_initialized = 1; | |
260 | ||
261 | return 0; | |
262 | } | |
263 | ||
264 | DEFINE_LSM(safesetid_security_init) = { | |
265 | .init = safesetid_security_init, | |
f67e20d2 | 266 | .name = "safesetid", |
aeca4e2c | 267 | }; |