devcg: propagate local changes down the hierarchy
[linux-3.10.git] / security / device_cgroup.c
index 19ecc8d..221967d 100644 (file)
 
 static DEFINE_MUTEX(devcgroup_mutex);
 
+enum devcg_behavior {
+       DEVCG_DEFAULT_NONE,
+       DEVCG_DEFAULT_ALLOW,
+       DEVCG_DEFAULT_DENY,
+};
+
 /*
  * exception list locking rules:
  * hold devcgroup_mutex for update/read.
@@ -42,10 +48,9 @@ struct dev_exception_item {
 struct dev_cgroup {
        struct cgroup_subsys_state css;
        struct list_head exceptions;
-       enum {
-               DEVCG_DEFAULT_ALLOW,
-               DEVCG_DEFAULT_DENY,
-       } behavior;
+       enum devcg_behavior behavior;
+       /* temporary list for pending propagation operations */
+       struct list_head propagate_pending;
 };
 
 static inline struct dev_cgroup *css_to_devcgroup(struct cgroup_subsys_state *s)
@@ -159,6 +164,16 @@ static void dev_exception_rm(struct dev_cgroup *dev_cgroup,
        }
 }
 
+static void __dev_exception_clean(struct dev_cgroup *dev_cgroup)
+{
+       struct dev_exception_item *ex, *tmp;
+
+       list_for_each_entry_safe(ex, tmp, &dev_cgroup->exceptions, list) {
+               list_del_rcu(&ex->list);
+               kfree_rcu(ex, rcu);
+       }
+}
+
 /**
  * dev_exception_clean - frees all entries of the exception list
  * @dev_cgroup: dev_cgroup with the exception list to be cleaned
@@ -167,14 +182,52 @@ static void dev_exception_rm(struct dev_cgroup *dev_cgroup,
  */
 static void dev_exception_clean(struct dev_cgroup *dev_cgroup)
 {
-       struct dev_exception_item *ex, *tmp;
-
        lockdep_assert_held(&devcgroup_mutex);
 
-       list_for_each_entry_safe(ex, tmp, &dev_cgroup->exceptions, list) {
-               list_del_rcu(&ex->list);
-               kfree_rcu(ex, rcu);
+       __dev_exception_clean(dev_cgroup);
+}
+
+static inline bool is_devcg_online(const struct dev_cgroup *devcg)
+{
+       return (devcg->behavior != DEVCG_DEFAULT_NONE);
+}
+
+/**
+ * devcgroup_online - initializes devcgroup's behavior and exceptions based on
+ *                   parent's
+ * @cgroup: cgroup getting online
+ * returns 0 in case of success, error code otherwise
+ */
+static int devcgroup_online(struct cgroup *cgroup)
+{
+       struct dev_cgroup *dev_cgroup, *parent_dev_cgroup = NULL;
+       int ret = 0;
+
+       mutex_lock(&devcgroup_mutex);
+       dev_cgroup = cgroup_to_devcgroup(cgroup);
+       if (cgroup->parent)
+               parent_dev_cgroup = cgroup_to_devcgroup(cgroup->parent);
+
+       if (parent_dev_cgroup == NULL)
+               dev_cgroup->behavior = DEVCG_DEFAULT_ALLOW;
+       else {
+               ret = dev_exceptions_copy(&dev_cgroup->exceptions,
+                                         &parent_dev_cgroup->exceptions);
+               if (!ret)
+                       dev_cgroup->behavior = parent_dev_cgroup->behavior;
        }
+       mutex_unlock(&devcgroup_mutex);
+
+       return ret;
+}
+
+static void devcgroup_offline(struct cgroup *cgroup)
+{
+       struct dev_cgroup *dev_cgroup = cgroup_to_devcgroup(cgroup);
+
+       mutex_lock(&devcgroup_mutex);
+       dev_cgroup->behavior = DEVCG_DEFAULT_NONE;
+       mutex_unlock(&devcgroup_mutex);
 }
 
 /*
@@ -182,31 +235,17 @@ static void dev_exception_clean(struct dev_cgroup *dev_cgroup)
  */
 static struct cgroup_subsys_state *devcgroup_css_alloc(struct cgroup *cgroup)
 {
-       struct dev_cgroup *dev_cgroup, *parent_dev_cgroup;
+       struct dev_cgroup *dev_cgroup;
        struct cgroup *parent_cgroup;
-       int ret;
 
        dev_cgroup = kzalloc(sizeof(*dev_cgroup), GFP_KERNEL);
        if (!dev_cgroup)
                return ERR_PTR(-ENOMEM);
        INIT_LIST_HEAD(&dev_cgroup->exceptions);
+       INIT_LIST_HEAD(&dev_cgroup->propagate_pending);
+       dev_cgroup->behavior = DEVCG_DEFAULT_NONE;
        parent_cgroup = cgroup->parent;
 
-       if (parent_cgroup == NULL)
-               dev_cgroup->behavior = DEVCG_DEFAULT_ALLOW;
-       else {
-               parent_dev_cgroup = cgroup_to_devcgroup(parent_cgroup);
-               mutex_lock(&devcgroup_mutex);
-               ret = dev_exceptions_copy(&dev_cgroup->exceptions,
-                                         &parent_dev_cgroup->exceptions);
-               dev_cgroup->behavior = parent_dev_cgroup->behavior;
-               mutex_unlock(&devcgroup_mutex);
-               if (ret) {
-                       kfree(dev_cgroup);
-                       return ERR_PTR(ret);
-               }
-       }
-
        return &dev_cgroup->css;
 }
 
@@ -215,7 +254,7 @@ static void devcgroup_css_free(struct cgroup *cgroup)
        struct dev_cgroup *dev_cgroup;
 
        dev_cgroup = cgroup_to_devcgroup(cgroup);
-       dev_exception_clean(dev_cgroup);
+       __dev_exception_clean(dev_cgroup);
        kfree(dev_cgroup);
 }
 
@@ -299,9 +338,11 @@ static int devcgroup_seq_read(struct cgroup *cgroup, struct cftype *cft,
  *             verify if a certain access is allowed.
  * @dev_cgroup: dev cgroup to be tested against
  * @refex: new exception
+ * @behavior: behavior of the exception
  */
-static int may_access(struct dev_cgroup *dev_cgroup,
-                     struct dev_exception_item *refex)
+static bool may_access(struct dev_cgroup *dev_cgroup,
+                      struct dev_exception_item *refex,
+                      enum devcg_behavior behavior)
 {
        struct dev_exception_item *ex;
        bool match = false;
@@ -325,18 +366,29 @@ static int may_access(struct dev_cgroup *dev_cgroup,
                break;
        }
 
-       /*
-        * In two cases we'll consider this new exception valid:
-        * - the dev cgroup has its default policy to allow + exception list:
-        *   the new exception should *not* match any of the exceptions
-        *   (behavior == DEVCG_DEFAULT_ALLOW, !match)
-        * - the dev cgroup has its default policy to deny + exception list:
-        *   the new exception *should* match the exceptions
-        *   (behavior == DEVCG_DEFAULT_DENY, match)
-        */
-       if ((dev_cgroup->behavior == DEVCG_DEFAULT_DENY) == match)
-               return 1;
-       return 0;
+       if (dev_cgroup->behavior == DEVCG_DEFAULT_ALLOW) {
+               if (behavior == DEVCG_DEFAULT_ALLOW) {
+                       /* the exception will deny access to certain devices */
+                       return true;
+               } else {
+                       /* the exception will allow access to certain devices */
+                       if (match)
+                               /*
+                                * a new exception allowing access shouldn't
+                                * match an parent's exception
+                                */
+                               return false;
+                       return true;
+               }
+       } else {
+               /* only behavior == DEVCG_DEFAULT_DENY allowed here */
+               if (match)
+                       /* parent has an exception that matches the proposed */
+                       return true;
+               else
+                       return false;
+       }
+       return false;
 }
 
 /*
@@ -353,7 +405,7 @@ static int parent_has_perm(struct dev_cgroup *childcg,
        if (!pcg)
                return 1;
        parent = cgroup_to_devcgroup(pcg);
-       return may_access(parent, ex);
+       return may_access(parent, ex, childcg->behavior);
 }
 
 /**
@@ -369,6 +421,111 @@ static inline int may_allow_all(struct dev_cgroup *parent)
        return parent->behavior == DEVCG_DEFAULT_ALLOW;
 }
 
+/**
+ * revalidate_active_exceptions - walks through the active exception list and
+ *                               revalidates the exceptions based on parent's
+ *                               behavior and exceptions. The exceptions that
+ *                               are no longer valid will be removed.
+ *                               Called with devcgroup_mutex held.
+ * @devcg: cgroup which exceptions will be checked
+ *
+ * This is one of the three key functions for hierarchy implementation.
+ * This function is responsible for re-evaluating all the cgroup's active
+ * exceptions due to a parent's exception change.
+ * Refer to Documentation/cgroups/devices.txt for more details.
+ */
+static void revalidate_active_exceptions(struct dev_cgroup *devcg)
+{
+       struct dev_exception_item *ex;
+       struct list_head *this, *tmp;
+
+       list_for_each_safe(this, tmp, &devcg->exceptions) {
+               ex = container_of(this, struct dev_exception_item, list);
+               if (!parent_has_perm(devcg, ex))
+                       dev_exception_rm(devcg, ex);
+       }
+}
+
+/**
+ * get_online_devcg - walks the cgroup tree and fills a list with the online
+ *                   groups
+ * @root: cgroup used as starting point
+ * @online: list that will be filled with online groups
+ *
+ * Must be called with devcgroup_mutex held. Grabs RCU lock.
+ * Because devcgroup_mutex is held, no devcg will become online or offline
+ * during the tree walk (see devcgroup_online, devcgroup_offline)
+ * A separated list is needed because propagate_behavior() and
+ * propagate_exception() need to allocate memory and can block.
+ */
+static void get_online_devcg(struct cgroup *root, struct list_head *online)
+{
+       struct cgroup *pos;
+       struct dev_cgroup *devcg;
+
+       lockdep_assert_held(&devcgroup_mutex);
+
+       rcu_read_lock();
+       cgroup_for_each_descendant_pre(pos, root) {
+               devcg = cgroup_to_devcgroup(pos);
+               if (is_devcg_online(devcg))
+                       list_add_tail(&devcg->propagate_pending, online);
+       }
+       rcu_read_unlock();
+}
+
+/**
+ * propagate_exception - propagates a new exception to the children
+ * @devcg_root: device cgroup that added a new exception
+ * @ex: new exception to be propagated
+ *
+ * returns: 0 in case of success, != 0 in case of error
+ */
+static int propagate_exception(struct dev_cgroup *devcg_root,
+                              struct dev_exception_item *ex)
+{
+       struct cgroup *root = devcg_root->css.cgroup;
+       struct dev_cgroup *devcg, *parent, *tmp;
+       int rc = 0;
+       LIST_HEAD(pending);
+
+       get_online_devcg(root, &pending);
+
+       list_for_each_entry_safe(devcg, tmp, &pending, propagate_pending) {
+               parent = cgroup_to_devcgroup(devcg->css.cgroup->parent);
+
+               /*
+                * in case both root's behavior and devcg is allow, a new
+                * restriction means adding to the exception list
+                */
+               if (devcg_root->behavior == DEVCG_DEFAULT_ALLOW &&
+                   devcg->behavior == DEVCG_DEFAULT_ALLOW) {
+                       rc = dev_exception_add(devcg, ex);
+                       if (rc)
+                               break;
+               } else {
+                       /*
+                        * in the other possible cases:
+                        * root's behavior: allow, devcg's: deny
+                        * root's behavior: deny, devcg's: deny
+                        * the exception will be removed
+                        */
+                       dev_exception_rm(devcg, ex);
+               }
+               revalidate_active_exceptions(devcg);
+
+               list_del_init(&devcg->propagate_pending);
+       }
+       return rc;
+}
+
+static inline bool has_children(struct dev_cgroup *devcgroup)
+{
+       struct cgroup *cgrp = devcgroup->css.cgroup;
+
+       return !list_empty(&cgrp->children);
+}
+
 /*
  * Modify the exception list using allow/deny rules.
  * CAP_SYS_ADMIN is needed for this.  It's at least separate from CAP_MKNOD
@@ -387,7 +544,7 @@ static int devcgroup_update_access(struct dev_cgroup *devcgroup,
 {
        const char *b;
        char temp[12];          /* 11 + 1 characters needed for a u32 */
-       int count, rc;
+       int count, rc = 0;
        struct dev_exception_item ex;
        struct cgroup *p = devcgroup->css.cgroup;
        struct dev_cgroup *parent = NULL;
@@ -405,6 +562,9 @@ static int devcgroup_update_access(struct dev_cgroup *devcgroup,
        case 'a':
                switch (filetype) {
                case DEVCG_ALLOW:
+                       if (has_children(devcgroup))
+                               return -EINVAL;
+
                        if (!may_allow_all(parent))
                                return -EPERM;
                        dev_exception_clean(devcgroup);
@@ -418,6 +578,9 @@ static int devcgroup_update_access(struct dev_cgroup *devcgroup,
                                return rc;
                        break;
                case DEVCG_DENY:
+                       if (has_children(devcgroup))
+                               return -EINVAL;
+
                        dev_exception_clean(devcgroup);
                        devcgroup->behavior = DEVCG_DEFAULT_DENY;
                        break;
@@ -512,22 +675,28 @@ static int devcgroup_update_access(struct dev_cgroup *devcgroup,
                        dev_exception_rm(devcgroup, &ex);
                        return 0;
                }
-               return dev_exception_add(devcgroup, &ex);
+               rc = dev_exception_add(devcgroup, &ex);
+               break;
        case DEVCG_DENY:
                /*
                 * If the default policy is to deny by default, try to remove
                 * an matching exception instead. And be silent about it: we
                 * don't want to break compatibility
                 */
-               if (devcgroup->behavior == DEVCG_DEFAULT_DENY) {
+               if (devcgroup->behavior == DEVCG_DEFAULT_DENY)
                        dev_exception_rm(devcgroup, &ex);
-                       return 0;
-               }
-               return dev_exception_add(devcgroup, &ex);
+               else
+                       rc = dev_exception_add(devcgroup, &ex);
+
+               if (rc)
+                       break;
+               /* we only propagate new restrictions */
+               rc = propagate_exception(devcgroup, &ex);
+               break;
        default:
-               return -EINVAL;
+               rc = -EINVAL;
        }
-       return 0;
+       return rc;
 }
 
 static int devcgroup_access_write(struct cgroup *cgrp, struct cftype *cft,
@@ -566,6 +735,8 @@ struct cgroup_subsys devices_subsys = {
        .can_attach = devcgroup_can_attach,
        .css_alloc = devcgroup_css_alloc,
        .css_free = devcgroup_css_free,
+       .css_online = devcgroup_online,
+       .css_offline = devcgroup_offline,
        .subsys_id = devices_subsys_id,
        .base_cftypes = dev_cgroup_files,
 
@@ -604,7 +775,7 @@ static int __devcgroup_check_permission(short type, u32 major, u32 minor,
 
        rcu_read_lock();
        dev_cgroup = task_devcgroup(current);
-       rc = may_access(dev_cgroup, &ex);
+       rc = may_access(dev_cgroup, &ex, dev_cgroup->behavior);
        rcu_read_unlock();
 
        if (!rc)