1aeca4e2cSMicah Morton // SPDX-License-Identifier: GPL-2.0
2aeca4e2cSMicah Morton /*
3aeca4e2cSMicah Morton * SafeSetID Linux Security Module
4aeca4e2cSMicah Morton *
5aeca4e2cSMicah Morton * Author: Micah Morton <mortonm@chromium.org>
6aeca4e2cSMicah Morton *
7aeca4e2cSMicah Morton * Copyright (C) 2018 The Chromium OS Authors.
8aeca4e2cSMicah Morton *
9aeca4e2cSMicah Morton * This program is free software; you can redistribute it and/or modify
10aeca4e2cSMicah Morton * it under the terms of the GNU General Public License version 2, as
11aeca4e2cSMicah Morton * published by the Free Software Foundation.
12aeca4e2cSMicah Morton *
13aeca4e2cSMicah Morton */
1403638e62SJann Horn
1503638e62SJann Horn #define pr_fmt(fmt) "SafeSetID: " fmt
1603638e62SJann Horn
17aeca4e2cSMicah Morton #include <linux/security.h>
18aeca4e2cSMicah Morton #include <linux/cred.h>
19aeca4e2cSMicah Morton
20aeca4e2cSMicah Morton #include "lsm.h"
21aeca4e2cSMicah Morton
225294bac9SThomas Cedeno static DEFINE_MUTEX(uid_policy_update_lock);
235294bac9SThomas Cedeno static DEFINE_MUTEX(gid_policy_update_lock);
24aeca4e2cSMicah Morton
25aeca4e2cSMicah Morton /*
265294bac9SThomas Cedeno * In the case the input buffer contains one or more invalid IDs, the kid_t
2778ae7df9SJann Horn * variables pointed to by @parent and @child will get updated but this
28aeca4e2cSMicah Morton * function will return an error.
2978ae7df9SJann Horn * Contents of @buf may be modified.
30aeca4e2cSMicah Morton */
parse_policy_line(struct file * file,char * buf,struct setid_rule * rule)3103638e62SJann Horn static int parse_policy_line(struct file *file, char *buf,
325294bac9SThomas Cedeno struct setid_rule *rule)
33aeca4e2cSMicah Morton {
3478ae7df9SJann Horn char *child_str;
35aeca4e2cSMicah Morton int ret;
3678ae7df9SJann Horn u32 parsed_parent, parsed_child;
37aeca4e2cSMicah Morton
385294bac9SThomas Cedeno /* Format of |buf| string should be <UID>:<UID> or <GID>:<GID> */
3978ae7df9SJann Horn child_str = strchr(buf, ':');
4078ae7df9SJann Horn if (child_str == NULL)
4178ae7df9SJann Horn return -EINVAL;
4278ae7df9SJann Horn *child_str = '\0';
4378ae7df9SJann Horn child_str++;
44aeca4e2cSMicah Morton
4578ae7df9SJann Horn ret = kstrtou32(buf, 0, &parsed_parent);
46aeca4e2cSMicah Morton if (ret)
4778ae7df9SJann Horn return ret;
48aeca4e2cSMicah Morton
4978ae7df9SJann Horn ret = kstrtou32(child_str, 0, &parsed_child);
50aeca4e2cSMicah Morton if (ret)
5178ae7df9SJann Horn return ret;
52aeca4e2cSMicah Morton
535294bac9SThomas Cedeno if (rule->type == UID){
545294bac9SThomas Cedeno rule->src_id.uid = make_kuid(file->f_cred->user_ns, parsed_parent);
555294bac9SThomas Cedeno rule->dst_id.uid = make_kuid(file->f_cred->user_ns, parsed_child);
565294bac9SThomas Cedeno if (!uid_valid(rule->src_id.uid) || !uid_valid(rule->dst_id.uid))
5778ae7df9SJann Horn return -EINVAL;
585294bac9SThomas Cedeno } else if (rule->type == GID){
595294bac9SThomas Cedeno rule->src_id.gid = make_kgid(file->f_cred->user_ns, parsed_parent);
605294bac9SThomas Cedeno rule->dst_id.gid = make_kgid(file->f_cred->user_ns, parsed_child);
615294bac9SThomas Cedeno if (!gid_valid(rule->src_id.gid) || !gid_valid(rule->dst_id.gid))
625294bac9SThomas Cedeno return -EINVAL;
635294bac9SThomas Cedeno } else {
645294bac9SThomas Cedeno /* Error, rule->type is an invalid type */
655294bac9SThomas Cedeno return -EINVAL;
665294bac9SThomas Cedeno }
6778ae7df9SJann Horn return 0;
68aeca4e2cSMicah Morton }
69aeca4e2cSMicah Morton
__release_ruleset(struct rcu_head * rcu)7003638e62SJann Horn static void __release_ruleset(struct rcu_head *rcu)
7178ae7df9SJann Horn {
725294bac9SThomas Cedeno struct setid_ruleset *pol =
735294bac9SThomas Cedeno container_of(rcu, struct setid_ruleset, rcu);
7403638e62SJann Horn int bucket;
755294bac9SThomas Cedeno struct setid_rule *rule;
7603638e62SJann Horn struct hlist_node *tmp;
7778ae7df9SJann Horn
7803638e62SJann Horn hash_for_each_safe(pol->rules, bucket, tmp, rule, next)
7903638e62SJann Horn kfree(rule);
80fbd9acb2SJann Horn kfree(pol->policy_str);
8103638e62SJann Horn kfree(pol);
8203638e62SJann Horn }
8303638e62SJann Horn
release_ruleset(struct setid_ruleset * pol)845294bac9SThomas Cedeno static void release_ruleset(struct setid_ruleset *pol){
8503638e62SJann Horn call_rcu(&pol->rcu, __release_ruleset);
8603638e62SJann Horn }
8703638e62SJann Horn
insert_rule(struct setid_ruleset * pol,struct setid_rule * rule)885294bac9SThomas Cedeno static void insert_rule(struct setid_ruleset *pol, struct setid_rule *rule)
894f72123dSJann Horn {
905294bac9SThomas Cedeno if (pol->type == UID)
915294bac9SThomas Cedeno hash_add(pol->rules, &rule->next, __kuid_val(rule->src_id.uid));
925294bac9SThomas Cedeno else if (pol->type == GID)
935294bac9SThomas Cedeno hash_add(pol->rules, &rule->next, __kgid_val(rule->src_id.gid));
945294bac9SThomas Cedeno else /* Error, pol->type is neither UID or GID */
955294bac9SThomas Cedeno return;
964f72123dSJann Horn }
974f72123dSJann Horn
verify_ruleset(struct setid_ruleset * pol)985294bac9SThomas Cedeno static int verify_ruleset(struct setid_ruleset *pol)
994f72123dSJann Horn {
1004f72123dSJann Horn int bucket;
1015294bac9SThomas Cedeno struct setid_rule *rule, *nrule;
1024f72123dSJann Horn int res = 0;
1034f72123dSJann Horn
1044f72123dSJann Horn hash_for_each(pol->rules, bucket, rule, next) {
1055294bac9SThomas Cedeno if (_setid_policy_lookup(pol, rule->dst_id, INVALID_ID) == SIDPOL_DEFAULT) {
1065294bac9SThomas Cedeno if (pol->type == UID) {
1074f72123dSJann Horn pr_warn("insecure policy detected: uid %d is constrained but transitively unconstrained through uid %d\n",
1085294bac9SThomas Cedeno __kuid_val(rule->src_id.uid),
1095294bac9SThomas Cedeno __kuid_val(rule->dst_id.uid));
1105294bac9SThomas Cedeno } else if (pol->type == GID) {
1115294bac9SThomas Cedeno pr_warn("insecure policy detected: gid %d is constrained but transitively unconstrained through gid %d\n",
1125294bac9SThomas Cedeno __kgid_val(rule->src_id.gid),
1135294bac9SThomas Cedeno __kgid_val(rule->dst_id.gid));
1145294bac9SThomas Cedeno } else { /* pol->type is an invalid type */
1155294bac9SThomas Cedeno res = -EINVAL;
1165294bac9SThomas Cedeno return res;
1175294bac9SThomas Cedeno }
1184f72123dSJann Horn res = -EINVAL;
1194f72123dSJann Horn
1204f72123dSJann Horn /* fix it up */
1215294bac9SThomas Cedeno nrule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL);
1224f72123dSJann Horn if (!nrule)
1234f72123dSJann Horn return -ENOMEM;
1245294bac9SThomas Cedeno if (pol->type == UID){
1255294bac9SThomas Cedeno nrule->src_id.uid = rule->dst_id.uid;
1265294bac9SThomas Cedeno nrule->dst_id.uid = rule->dst_id.uid;
1275294bac9SThomas Cedeno nrule->type = UID;
1285294bac9SThomas Cedeno } else { /* pol->type must be GID if we've made it to here */
1295294bac9SThomas Cedeno nrule->src_id.gid = rule->dst_id.gid;
1305294bac9SThomas Cedeno nrule->dst_id.gid = rule->dst_id.gid;
1315294bac9SThomas Cedeno nrule->type = GID;
1325294bac9SThomas Cedeno }
1334f72123dSJann Horn insert_rule(pol, nrule);
1344f72123dSJann Horn }
1354f72123dSJann Horn }
1364f72123dSJann Horn return res;
1374f72123dSJann Horn }
1384f72123dSJann Horn
handle_policy_update(struct file * file,const char __user * ubuf,size_t len,enum setid_type policy_type)13903638e62SJann Horn static ssize_t handle_policy_update(struct file *file,
1405294bac9SThomas Cedeno const char __user *ubuf, size_t len, enum setid_type policy_type)
14103638e62SJann Horn {
1425294bac9SThomas Cedeno struct setid_ruleset *pol;
14303638e62SJann Horn char *buf, *p, *end;
14403638e62SJann Horn int err;
14503638e62SJann Horn
1465294bac9SThomas Cedeno pol = kmalloc(sizeof(struct setid_ruleset), GFP_KERNEL);
14703638e62SJann Horn if (!pol)
14803638e62SJann Horn return -ENOMEM;
149fbd9acb2SJann Horn pol->policy_str = NULL;
1505294bac9SThomas Cedeno pol->type = policy_type;
15103638e62SJann Horn hash_init(pol->rules);
15203638e62SJann Horn
15303638e62SJann Horn p = buf = memdup_user_nul(ubuf, len);
15403638e62SJann Horn if (IS_ERR(buf)) {
15503638e62SJann Horn err = PTR_ERR(buf);
15603638e62SJann Horn goto out_free_pol;
15703638e62SJann Horn }
158fbd9acb2SJann Horn pol->policy_str = kstrdup(buf, GFP_KERNEL);
159fbd9acb2SJann Horn if (pol->policy_str == NULL) {
160fbd9acb2SJann Horn err = -ENOMEM;
161fbd9acb2SJann Horn goto out_free_buf;
162fbd9acb2SJann Horn }
16303638e62SJann Horn
16403638e62SJann Horn /* policy lines, including the last one, end with \n */
16503638e62SJann Horn while (*p != '\0') {
1665294bac9SThomas Cedeno struct setid_rule *rule;
16703638e62SJann Horn
16803638e62SJann Horn end = strchr(p, '\n');
16903638e62SJann Horn if (end == NULL) {
17003638e62SJann Horn err = -EINVAL;
17103638e62SJann Horn goto out_free_buf;
17203638e62SJann Horn }
17303638e62SJann Horn *end = '\0';
17403638e62SJann Horn
1755294bac9SThomas Cedeno rule = kmalloc(sizeof(struct setid_rule), GFP_KERNEL);
17603638e62SJann Horn if (!rule) {
17703638e62SJann Horn err = -ENOMEM;
17803638e62SJann Horn goto out_free_buf;
17903638e62SJann Horn }
18003638e62SJann Horn
1815294bac9SThomas Cedeno rule->type = policy_type;
18203638e62SJann Horn err = parse_policy_line(file, p, rule);
18303638e62SJann Horn if (err)
18403638e62SJann Horn goto out_free_rule;
18503638e62SJann Horn
1865294bac9SThomas Cedeno if (_setid_policy_lookup(pol, rule->src_id, rule->dst_id) == SIDPOL_ALLOWED) {
18703638e62SJann Horn pr_warn("bad policy: duplicate entry\n");
18803638e62SJann Horn err = -EEXIST;
18903638e62SJann Horn goto out_free_rule;
19003638e62SJann Horn }
19103638e62SJann Horn
1924f72123dSJann Horn insert_rule(pol, rule);
19303638e62SJann Horn p = end + 1;
19403638e62SJann Horn continue;
19503638e62SJann Horn
19603638e62SJann Horn out_free_rule:
19703638e62SJann Horn kfree(rule);
19803638e62SJann Horn goto out_free_buf;
19903638e62SJann Horn }
20003638e62SJann Horn
2014f72123dSJann Horn err = verify_ruleset(pol);
2024f72123dSJann Horn /* bogus policy falls through after fixing it up */
2034f72123dSJann Horn if (err && err != -EINVAL)
2044f72123dSJann Horn goto out_free_buf;
2054f72123dSJann Horn
20603638e62SJann Horn /*
20703638e62SJann Horn * Everything looks good, apply the policy and release the old one.
20803638e62SJann Horn * What we really want here is an xchg() wrapper for RCU, but since that
20903638e62SJann Horn * doesn't currently exist, just use a spinlock for now.
21003638e62SJann Horn */
2115294bac9SThomas Cedeno if (policy_type == UID) {
2125294bac9SThomas Cedeno mutex_lock(&uid_policy_update_lock);
213a60a5746SPaul E. McKenney pol = rcu_replace_pointer(safesetid_setuid_rules, pol,
2145294bac9SThomas Cedeno lockdep_is_held(&uid_policy_update_lock));
2155294bac9SThomas Cedeno mutex_unlock(&uid_policy_update_lock);
2165294bac9SThomas Cedeno } else if (policy_type == GID) {
2175294bac9SThomas Cedeno mutex_lock(&gid_policy_update_lock);
2185294bac9SThomas Cedeno pol = rcu_replace_pointer(safesetid_setgid_rules, pol,
2195294bac9SThomas Cedeno lockdep_is_held(&gid_policy_update_lock));
2205294bac9SThomas Cedeno mutex_unlock(&gid_policy_update_lock);
2215294bac9SThomas Cedeno } else {
2225294bac9SThomas Cedeno /* Error, policy type is neither UID or GID */
2235294bac9SThomas Cedeno pr_warn("error: bad policy type");
2245294bac9SThomas Cedeno }
22503638e62SJann Horn err = len;
22603638e62SJann Horn
22703638e62SJann Horn out_free_buf:
22803638e62SJann Horn kfree(buf);
22903638e62SJann Horn out_free_pol:
23021ab8580SMicah Morton if (pol)
23103638e62SJann Horn release_ruleset(pol);
23203638e62SJann Horn return err;
233aeca4e2cSMicah Morton }
234aeca4e2cSMicah Morton
safesetid_uid_file_write(struct file * file,const char __user * buf,size_t len,loff_t * ppos)2355294bac9SThomas Cedeno static ssize_t safesetid_uid_file_write(struct file *file,
236aeca4e2cSMicah Morton const char __user *buf,
237aeca4e2cSMicah Morton size_t len,
238aeca4e2cSMicah Morton loff_t *ppos)
239aeca4e2cSMicah Morton {
24071a98971SJann Horn if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN))
241aeca4e2cSMicah Morton return -EPERM;
242aeca4e2cSMicah Morton
243aeca4e2cSMicah Morton if (*ppos != 0)
244aeca4e2cSMicah Morton return -EINVAL;
245aeca4e2cSMicah Morton
2465294bac9SThomas Cedeno return handle_policy_update(file, buf, len, UID);
2475294bac9SThomas Cedeno }
2485294bac9SThomas Cedeno
safesetid_gid_file_write(struct file * file,const char __user * buf,size_t len,loff_t * ppos)2495294bac9SThomas Cedeno static ssize_t safesetid_gid_file_write(struct file *file,
2505294bac9SThomas Cedeno const char __user *buf,
2515294bac9SThomas Cedeno size_t len,
2525294bac9SThomas Cedeno loff_t *ppos)
2535294bac9SThomas Cedeno {
2545294bac9SThomas Cedeno if (!file_ns_capable(file, &init_user_ns, CAP_MAC_ADMIN))
2555294bac9SThomas Cedeno return -EPERM;
2565294bac9SThomas Cedeno
2575294bac9SThomas Cedeno if (*ppos != 0)
2585294bac9SThomas Cedeno return -EINVAL;
2595294bac9SThomas Cedeno
2605294bac9SThomas Cedeno return handle_policy_update(file, buf, len, GID);
261aeca4e2cSMicah Morton }
262aeca4e2cSMicah Morton
safesetid_file_read(struct file * file,char __user * buf,size_t len,loff_t * ppos,struct mutex * policy_update_lock,struct __rcu setid_ruleset * ruleset)263fbd9acb2SJann Horn static ssize_t safesetid_file_read(struct file *file, char __user *buf,
26403ca0ec1SThomas Cedeno size_t len, loff_t *ppos, struct mutex *policy_update_lock, struct __rcu setid_ruleset* ruleset)
265fbd9acb2SJann Horn {
266fbd9acb2SJann Horn ssize_t res = 0;
2675294bac9SThomas Cedeno struct setid_ruleset *pol;
268fbd9acb2SJann Horn const char *kbuf;
269fbd9acb2SJann Horn
2705294bac9SThomas Cedeno mutex_lock(policy_update_lock);
2715294bac9SThomas Cedeno pol = rcu_dereference_protected(ruleset, lockdep_is_held(policy_update_lock));
272fbd9acb2SJann Horn if (pol) {
273fbd9acb2SJann Horn kbuf = pol->policy_str;
274fbd9acb2SJann Horn res = simple_read_from_buffer(buf, len, ppos,
275fbd9acb2SJann Horn kbuf, strlen(kbuf));
276fbd9acb2SJann Horn }
2775294bac9SThomas Cedeno mutex_unlock(policy_update_lock);
2785294bac9SThomas Cedeno
279fbd9acb2SJann Horn return res;
280fbd9acb2SJann Horn }
281fbd9acb2SJann Horn
safesetid_uid_file_read(struct file * file,char __user * buf,size_t len,loff_t * ppos)2825294bac9SThomas Cedeno static ssize_t safesetid_uid_file_read(struct file *file, char __user *buf,
2835294bac9SThomas Cedeno size_t len, loff_t *ppos)
2845294bac9SThomas Cedeno {
2855294bac9SThomas Cedeno return safesetid_file_read(file, buf, len, ppos,
2865294bac9SThomas Cedeno &uid_policy_update_lock, safesetid_setuid_rules);
2875294bac9SThomas Cedeno }
2885294bac9SThomas Cedeno
safesetid_gid_file_read(struct file * file,char __user * buf,size_t len,loff_t * ppos)2895294bac9SThomas Cedeno static ssize_t safesetid_gid_file_read(struct file *file, char __user *buf,
2905294bac9SThomas Cedeno size_t len, loff_t *ppos)
2915294bac9SThomas Cedeno {
2925294bac9SThomas Cedeno return safesetid_file_read(file, buf, len, ppos,
2935294bac9SThomas Cedeno &gid_policy_update_lock, safesetid_setgid_rules);
2945294bac9SThomas Cedeno }
2955294bac9SThomas Cedeno
2965294bac9SThomas Cedeno
2975294bac9SThomas Cedeno
2985294bac9SThomas Cedeno static const struct file_operations safesetid_uid_file_fops = {
2995294bac9SThomas Cedeno .read = safesetid_uid_file_read,
3005294bac9SThomas Cedeno .write = safesetid_uid_file_write,
3015294bac9SThomas Cedeno };
3025294bac9SThomas Cedeno
3035294bac9SThomas Cedeno static const struct file_operations safesetid_gid_file_fops = {
3045294bac9SThomas Cedeno .read = safesetid_gid_file_read,
3055294bac9SThomas Cedeno .write = safesetid_gid_file_write,
306aeca4e2cSMicah Morton };
307aeca4e2cSMicah Morton
safesetid_init_securityfs(void)308aeca4e2cSMicah Morton static int __init safesetid_init_securityfs(void)
309aeca4e2cSMicah Morton {
310aeca4e2cSMicah Morton int ret;
31103638e62SJann Horn struct dentry *policy_dir;
3125294bac9SThomas Cedeno struct dentry *uid_policy_file;
3135294bac9SThomas Cedeno struct dentry *gid_policy_file;
314aeca4e2cSMicah Morton
315aeca4e2cSMicah Morton if (!safesetid_initialized)
316aeca4e2cSMicah Morton return 0;
317aeca4e2cSMicah Morton
31803638e62SJann Horn policy_dir = securityfs_create_dir("safesetid", NULL);
31903638e62SJann Horn if (IS_ERR(policy_dir)) {
32003638e62SJann Horn ret = PTR_ERR(policy_dir);
321aeca4e2cSMicah Morton goto error;
322aeca4e2cSMicah Morton }
323aeca4e2cSMicah Morton
3245294bac9SThomas Cedeno uid_policy_file = securityfs_create_file("uid_allowlist_policy", 0600,
3255294bac9SThomas Cedeno policy_dir, NULL, &safesetid_uid_file_fops);
3265294bac9SThomas Cedeno if (IS_ERR(uid_policy_file)) {
3275294bac9SThomas Cedeno ret = PTR_ERR(uid_policy_file);
328aeca4e2cSMicah Morton goto error;
329aeca4e2cSMicah Morton }
330aeca4e2cSMicah Morton
3315294bac9SThomas Cedeno gid_policy_file = securityfs_create_file("gid_allowlist_policy", 0600,
3325294bac9SThomas Cedeno policy_dir, NULL, &safesetid_gid_file_fops);
3335294bac9SThomas Cedeno if (IS_ERR(gid_policy_file)) {
3345294bac9SThomas Cedeno ret = PTR_ERR(gid_policy_file);
3355294bac9SThomas Cedeno goto error;
3365294bac9SThomas Cedeno }
3375294bac9SThomas Cedeno
3385294bac9SThomas Cedeno
339aeca4e2cSMicah Morton return 0;
340aeca4e2cSMicah Morton
341aeca4e2cSMicah Morton error:
34203638e62SJann Horn securityfs_remove(policy_dir);
343aeca4e2cSMicah Morton return ret;
344aeca4e2cSMicah Morton }
345aeca4e2cSMicah Morton fs_initcall(safesetid_init_securityfs);
346