diff options
Diffstat (limited to 'security/safesetid/securityfs.c')
-rw-r--r-- | security/safesetid/securityfs.c | 307 |
1 files changed, 187 insertions, 120 deletions
diff --git a/security/safesetid/securityfs.c b/security/safesetid/securityfs.c index 2c6c829be044..d568e17dd773 100644 --- a/security/safesetid/securityfs.c +++ b/security/safesetid/securityfs.c | |||
@@ -11,92 +11,184 @@ | |||
11 | * published by the Free Software Foundation. | 11 | * published by the Free Software Foundation. |
12 | * | 12 | * |
13 | */ | 13 | */ |
14 | |||
15 | #define pr_fmt(fmt) "SafeSetID: " fmt | ||
16 | |||
14 | #include <linux/security.h> | 17 | #include <linux/security.h> |
15 | #include <linux/cred.h> | 18 | #include <linux/cred.h> |
16 | 19 | ||
17 | #include "lsm.h" | 20 | #include "lsm.h" |
18 | 21 | ||
19 | static struct dentry *safesetid_policy_dir; | 22 | static DEFINE_MUTEX(policy_update_lock); |
20 | |||
21 | struct safesetid_file_entry { | ||
22 | const char *name; | ||
23 | enum safesetid_whitelist_file_write_type type; | ||
24 | struct dentry *dentry; | ||
25 | }; | ||
26 | |||
27 | static struct safesetid_file_entry safesetid_files[] = { | ||
28 | {.name = "add_whitelist_policy", | ||
29 | .type = SAFESETID_WHITELIST_ADD}, | ||
30 | {.name = "flush_whitelist_policies", | ||
31 | .type = SAFESETID_WHITELIST_FLUSH}, | ||
32 | }; | ||
33 | 23 | ||
34 | /* | 24 | /* |
35 | * In the case the input buffer contains one or more invalid UIDs, the kuid_t | 25 | * In the case the input buffer contains one or more invalid UIDs, the kuid_t |
36 | * variables pointed to by 'parent' and 'child' will get updated but this | 26 | * variables pointed to by @parent and @child will get updated but this |
37 | * function will return an error. | 27 | * function will return an error. |
28 | * Contents of @buf may be modified. | ||
38 | */ | 29 | */ |
39 | static int parse_safesetid_whitelist_policy(const char __user *buf, | 30 | static int parse_policy_line(struct file *file, char *buf, |
40 | size_t len, | 31 | struct setuid_rule *rule) |
41 | kuid_t *parent, | ||
42 | kuid_t *child) | ||
43 | { | 32 | { |
44 | char *kern_buf; | 33 | char *child_str; |
45 | char *parent_buf; | ||
46 | char *child_buf; | ||
47 | const char separator[] = ":"; | ||
48 | int ret; | 34 | int ret; |
49 | size_t first_substring_length; | 35 | u32 parsed_parent, parsed_child; |
50 | long parsed_parent; | ||
51 | long parsed_child; | ||
52 | 36 | ||
53 | /* Duplicate string from user memory and NULL-terminate */ | 37 | /* Format of |buf| string should be <UID>:<UID>. */ |
54 | kern_buf = memdup_user_nul(buf, len); | 38 | child_str = strchr(buf, ':'); |
55 | if (IS_ERR(kern_buf)) | 39 | if (child_str == NULL) |
56 | return PTR_ERR(kern_buf); | 40 | return -EINVAL; |
41 | *child_str = '\0'; | ||
42 | child_str++; | ||
57 | 43 | ||
58 | /* | 44 | ret = kstrtou32(buf, 0, &parsed_parent); |
59 | * Format of |buf| string should be <UID>:<UID>. | 45 | if (ret) |
60 | * Find location of ":" in kern_buf (copied from |buf|). | 46 | return ret; |
61 | */ | 47 | |
62 | first_substring_length = strcspn(kern_buf, separator); | 48 | ret = kstrtou32(child_str, 0, &parsed_child); |
63 | if (first_substring_length == 0 || first_substring_length == len) { | 49 | if (ret) |
64 | ret = -EINVAL; | 50 | return ret; |
65 | goto free_kern; | ||
66 | } | ||
67 | 51 | ||
68 | parent_buf = kmemdup_nul(kern_buf, first_substring_length, GFP_KERNEL); | 52 | rule->src_uid = make_kuid(file->f_cred->user_ns, parsed_parent); |
69 | if (!parent_buf) { | 53 | rule->dst_uid = make_kuid(file->f_cred->user_ns, parsed_child); |
70 | ret = -ENOMEM; | 54 | if (!uid_valid(rule->src_uid) || !uid_valid(rule->dst_uid)) |
71 | goto free_kern; | 55 | return -EINVAL; |
56 | |||
57 | return 0; | ||
58 | } | ||
59 | |||
60 | static void __release_ruleset(struct rcu_head *rcu) | ||
61 | { | ||
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); | ||
70 | kfree(pol->policy_str); | ||
71 | kfree(pol); | ||
72 | } | ||
73 | |||
74 | static void release_ruleset(struct setuid_ruleset *pol) | ||
75 | { | ||
76 | call_rcu(&pol->rcu, __release_ruleset); | ||
77 | } | ||
78 | |||
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 | } | ||
72 | } | 106 | } |
107 | return res; | ||
108 | } | ||
73 | 109 | ||
74 | ret = kstrtol(parent_buf, 0, &parsed_parent); | 110 | static ssize_t handle_policy_update(struct file *file, |
75 | if (ret) | 111 | const char __user *ubuf, size_t len) |
76 | goto free_both; | 112 | { |
113 | struct setuid_ruleset *pol; | ||
114 | char *buf, *p, *end; | ||
115 | int err; | ||
77 | 116 | ||
78 | child_buf = kern_buf + first_substring_length + 1; | 117 | pol = kmalloc(sizeof(struct setuid_ruleset), GFP_KERNEL); |
79 | ret = kstrtol(child_buf, 0, &parsed_child); | 118 | if (!pol) |
80 | if (ret) | 119 | return -ENOMEM; |
81 | goto free_both; | 120 | pol->policy_str = NULL; |
121 | hash_init(pol->rules); | ||
82 | 122 | ||
83 | *parent = make_kuid(current_user_ns(), parsed_parent); | 123 | p = buf = memdup_user_nul(ubuf, len); |
84 | if (!uid_valid(*parent)) { | 124 | if (IS_ERR(buf)) { |
85 | ret = -EINVAL; | 125 | err = PTR_ERR(buf); |
86 | goto free_both; | 126 | goto out_free_pol; |
87 | } | 127 | } |
128 | pol->policy_str = kstrdup(buf, GFP_KERNEL); | ||
129 | if (pol->policy_str == NULL) { | ||
130 | err = -ENOMEM; | ||
131 | goto out_free_buf; | ||
132 | } | ||
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 | } | ||
88 | 150 | ||
89 | *child = make_kuid(current_user_ns(), parsed_child); | 151 | err = parse_policy_line(file, p, rule); |
90 | if (!uid_valid(*child)) { | 152 | if (err) |
91 | ret = -EINVAL; | 153 | goto out_free_rule; |
92 | goto free_both; | 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 | |||
162 | insert_rule(pol, rule); | ||
163 | p = end + 1; | ||
164 | continue; | ||
165 | |||
166 | out_free_rule: | ||
167 | kfree(rule); | ||
168 | goto out_free_buf; | ||
93 | } | 169 | } |
94 | 170 | ||
95 | free_both: | 171 | err = verify_ruleset(pol); |
96 | kfree(parent_buf); | 172 | /* bogus policy falls through after fixing it up */ |
97 | free_kern: | 173 | if (err && err != -EINVAL) |
98 | kfree(kern_buf); | 174 | goto out_free_buf; |
99 | return ret; | 175 | |
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 | */ | ||
181 | mutex_lock(&policy_update_lock); | ||
182 | rcu_swap_protected(safesetid_setuid_rules, pol, | ||
183 | lockdep_is_held(&policy_update_lock)); | ||
184 | mutex_unlock(&policy_update_lock); | ||
185 | err = len; | ||
186 | |||
187 | out_free_buf: | ||
188 | kfree(buf); | ||
189 | out_free_pol: | ||
190 | release_ruleset(pol); | ||
191 | return err; | ||
100 | } | 192 | } |
101 | 193 | ||
102 | static ssize_t safesetid_file_write(struct file *file, | 194 | static ssize_t safesetid_file_write(struct file *file, |
@@ -104,90 +196,65 @@ static ssize_t safesetid_file_write(struct file *file, | |||
104 | size_t len, | 196 | size_t len, |
105 | loff_t *ppos) | 197 | loff_t *ppos) |
106 | { | 198 | { |
107 | struct safesetid_file_entry *file_entry = | 199 | if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN)) |
108 | file->f_inode->i_private; | ||
109 | kuid_t parent; | ||
110 | kuid_t child; | ||
111 | int ret; | ||
112 | |||
113 | if (!ns_capable(current_user_ns(), CAP_MAC_ADMIN)) | ||
114 | return -EPERM; | 200 | return -EPERM; |
115 | 201 | ||
116 | if (*ppos != 0) | 202 | if (*ppos != 0) |
117 | return -EINVAL; | 203 | return -EINVAL; |
118 | 204 | ||
119 | switch (file_entry->type) { | 205 | return handle_policy_update(file, buf, len); |
120 | case SAFESETID_WHITELIST_FLUSH: | ||
121 | flush_safesetid_whitelist_entries(); | ||
122 | break; | ||
123 | case SAFESETID_WHITELIST_ADD: | ||
124 | ret = parse_safesetid_whitelist_policy(buf, len, &parent, | ||
125 | &child); | ||
126 | if (ret) | ||
127 | return ret; | ||
128 | |||
129 | ret = add_safesetid_whitelist_entry(parent, child); | ||
130 | if (ret) | ||
131 | return ret; | ||
132 | break; | ||
133 | default: | ||
134 | pr_warn("Unknown securityfs file %d\n", file_entry->type); | ||
135 | break; | ||
136 | } | ||
137 | |||
138 | /* Return len on success so caller won't keep trying to write */ | ||
139 | return len; | ||
140 | } | 206 | } |
141 | 207 | ||
142 | static const struct file_operations safesetid_file_fops = { | 208 | static ssize_t safesetid_file_read(struct file *file, char __user *buf, |
143 | .write = safesetid_file_write, | 209 | size_t len, loff_t *ppos) |
144 | }; | ||
145 | |||
146 | static void safesetid_shutdown_securityfs(void) | ||
147 | { | 210 | { |
148 | int i; | 211 | ssize_t res = 0; |
212 | struct setuid_ruleset *pol; | ||
213 | const char *kbuf; | ||
149 | 214 | ||
150 | for (i = 0; i < ARRAY_SIZE(safesetid_files); ++i) { | 215 | mutex_lock(&policy_update_lock); |
151 | struct safesetid_file_entry *entry = | 216 | pol = rcu_dereference_protected(safesetid_setuid_rules, |
152 | &safesetid_files[i]; | 217 | lockdep_is_held(&policy_update_lock)); |
153 | securityfs_remove(entry->dentry); | 218 | if (pol) { |
154 | entry->dentry = NULL; | 219 | kbuf = pol->policy_str; |
220 | res = simple_read_from_buffer(buf, len, ppos, | ||
221 | kbuf, strlen(kbuf)); | ||
155 | } | 222 | } |
156 | 223 | mutex_unlock(&policy_update_lock); | |
157 | securityfs_remove(safesetid_policy_dir); | 224 | return res; |
158 | safesetid_policy_dir = NULL; | ||
159 | } | 225 | } |
160 | 226 | ||
227 | static const struct file_operations safesetid_file_fops = { | ||
228 | .read = safesetid_file_read, | ||
229 | .write = safesetid_file_write, | ||
230 | }; | ||
231 | |||
161 | static int __init safesetid_init_securityfs(void) | 232 | static int __init safesetid_init_securityfs(void) |
162 | { | 233 | { |
163 | int i; | ||
164 | int ret; | 234 | int ret; |
235 | struct dentry *policy_dir; | ||
236 | struct dentry *policy_file; | ||
165 | 237 | ||
166 | if (!safesetid_initialized) | 238 | if (!safesetid_initialized) |
167 | return 0; | 239 | return 0; |
168 | 240 | ||
169 | safesetid_policy_dir = securityfs_create_dir("safesetid", NULL); | 241 | policy_dir = securityfs_create_dir("safesetid", NULL); |
170 | if (IS_ERR(safesetid_policy_dir)) { | 242 | if (IS_ERR(policy_dir)) { |
171 | ret = PTR_ERR(safesetid_policy_dir); | 243 | ret = PTR_ERR(policy_dir); |
172 | goto error; | 244 | goto error; |
173 | } | 245 | } |
174 | 246 | ||
175 | for (i = 0; i < ARRAY_SIZE(safesetid_files); ++i) { | 247 | policy_file = securityfs_create_file("whitelist_policy", 0600, |
176 | struct safesetid_file_entry *entry = | 248 | policy_dir, NULL, &safesetid_file_fops); |
177 | &safesetid_files[i]; | 249 | if (IS_ERR(policy_file)) { |
178 | entry->dentry = securityfs_create_file( | 250 | ret = PTR_ERR(policy_file); |
179 | entry->name, 0200, safesetid_policy_dir, | 251 | goto error; |
180 | entry, &safesetid_file_fops); | ||
181 | if (IS_ERR(entry->dentry)) { | ||
182 | ret = PTR_ERR(entry->dentry); | ||
183 | goto error; | ||
184 | } | ||
185 | } | 252 | } |
186 | 253 | ||
187 | return 0; | 254 | return 0; |
188 | 255 | ||
189 | error: | 256 | error: |
190 | safesetid_shutdown_securityfs(); | 257 | securityfs_remove(policy_dir); |
191 | return ret; | 258 | return ret; |
192 | } | 259 | } |
193 | fs_initcall(safesetid_init_securityfs); | 260 | fs_initcall(safesetid_init_securityfs); |