Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

devcg: propagate local changes down the hierarchy

This patch makes exception changes to propagate down in hierarchy respecting
when possible local exceptions.

New exceptions allowing additional access to devices won't be propagated, but
it'll be possible to add an exception to access all of part of the newly
allowed device(s).

New exceptions disallowing access to devices will be propagated down and the
local group's exceptions will be revalidated for the new situation.
Example:
A
/ \
B

group behavior exceptions
A allow "b 8:* rwm", "c 116:1 rw"
B deny "c 1:3 rwm", "c 116:2 rwm", "b 3:* rwm"

If a new exception is added to group A:
# echo "c 116:* r" > A/devices.deny
it'll propagate down and after revalidating B's local exceptions, the exception
"c 116:2 rwm" will be removed.

In case parent's exceptions change and local exceptions are not allowed anymore,
they'll be deleted.

v7:
- do not allow behavior change when the cgroup has children
- update documentation

v6: fixed issues pointed by Serge Hallyn
- only copy parent's exceptions while propagating behavior if the local
behavior is different
- while propagating exceptions, do not clear and copy parent's: it'd be against
the premise we don't propagate access to more devices

v5: fixed issues pointed by Serge Hallyn
- updated documentation
- not propagating when an exception is written to devices.allow
- when propagating a new behavior, clean the local exceptions list if they're
for a different behavior

v4: fixed issues pointed by Tejun Heo
- separated function to walk the tree and collect valid propagation targets

v3: fixed issues pointed by Tejun Heo
- update documentation
- move css_online/css_offline changes to a new patch
- use cgroup_for_each_descendant_pre() instead of own descendant walk
- move exception_copy rework to a separared patch
- move exception_clean rework to a separated patch

v2: fixed issues pointed by Tejun Heo
- instead of keeping the local settings that won't apply anymore, remove them

Cc: Tejun Heo <tj@kernel.org>
Cc: Serge Hallyn <serge.hallyn@canonical.com>
Signed-off-by: Aristeu Rozanski <aris@redhat.com>
Signed-off-by: Tejun Heo <tj@kernel.org>

authored by

Aristeu Rozanski and committed by
Tejun Heo
bd2953eb 1909554c

+199 -10
+67 -3
Documentation/cgroups/devices.txt
··· 13 13 The root device cgroup starts with rwm to 'all'. A child device 14 14 cgroup gets a copy of the parent. Administrators can then remove 15 15 devices from the whitelist or add new entries. A child cgroup can 16 - never receive a device access which is denied by its parent. However 17 - when a device access is removed from a parent it will not also be 18 - removed from the child(ren). 16 + never receive a device access which is denied by its parent. 19 17 20 18 2. User Interface 21 19 ··· 48 50 49 51 A cgroup may not be granted more permissions than the cgroup's 50 52 parent has. 53 + 54 + 4. Hierarchy 55 + 56 + device cgroups maintain hierarchy by making sure a cgroup never has more 57 + access permissions than its parent. Every time an entry is written to 58 + a cgroup's devices.deny file, all its children will have that entry removed 59 + from their whitelist and all the locally set whitelist entries will be 60 + re-evaluated. In case one of the locally set whitelist entries would provide 61 + more access than the cgroup's parent, it'll be removed from the whitelist. 62 + 63 + Example: 64 + A 65 + / \ 66 + B 67 + 68 + group behavior exceptions 69 + A allow "b 8:* rwm", "c 116:1 rw" 70 + B deny "c 1:3 rwm", "c 116:2 rwm", "b 3:* rwm" 71 + 72 + If a device is denied in group A: 73 + # echo "c 116:* r" > A/devices.deny 74 + it'll propagate down and after revalidating B's entries, the whitelist entry 75 + "c 116:2 rwm" will be removed: 76 + 77 + group whitelist entries denied devices 78 + A all "b 8:* rwm", "c 116:* rw" 79 + B "c 1:3 rwm", "b 3:* rwm" all the rest 80 + 81 + In case parent's exceptions change and local exceptions are not allowed 82 + anymore, they'll be deleted. 83 + 84 + Notice that new whitelist entries will not be propagated: 85 + A 86 + / \ 87 + B 88 + 89 + group whitelist entries denied devices 90 + A "c 1:3 rwm", "c 1:5 r" all the rest 91 + B "c 1:3 rwm", "c 1:5 r" all the rest 92 + 93 + when adding "c *:3 rwm": 94 + # echo "c *:3 rwm" >A/devices.allow 95 + 96 + the result: 97 + group whitelist entries denied devices 98 + A "c *:3 rwm", "c 1:5 r" all the rest 99 + B "c 1:3 rwm", "c 1:5 r" all the rest 100 + 101 + but now it'll be possible to add new entries to B: 102 + # echo "c 2:3 rwm" >B/devices.allow 103 + # echo "c 50:3 r" >B/devices.allow 104 + or even 105 + # echo "c *:3 rwm" >B/devices.allow 106 + 107 + Allowing or denying all by writing 'a' to devices.allow or devices.deny will 108 + not be possible once the device cgroups has children. 109 + 110 + 4.1 Hierarchy (internal implementation) 111 + 112 + device cgroups is implemented internally using a behavior (ALLOW, DENY) and a 113 + list of exceptions. The internal state is controlled using the same user 114 + interface to preserve compatibility with the previous whitelist-only 115 + implementation. Removal or addition of exceptions that will reduce the access 116 + to devices will be propagated down the hierarchy. 117 + For every propagated exception, the effective rules will be re-evaluated based 118 + on current parent's access rules.
+132 -7
security/device_cgroup.c
··· 49 49 struct cgroup_subsys_state css; 50 50 struct list_head exceptions; 51 51 enum devcg_behavior behavior; 52 + /* temporary list for pending propagation operations */ 53 + struct list_head propagate_pending; 52 54 }; 53 55 54 56 static inline struct dev_cgroup *css_to_devcgroup(struct cgroup_subsys_state *s) ··· 187 185 __dev_exception_clean(dev_cgroup); 188 186 } 189 187 188 + static inline bool is_devcg_online(const struct dev_cgroup *devcg) 189 + { 190 + return (devcg->behavior != DEVCG_DEFAULT_NONE); 191 + } 192 + 190 193 /** 191 194 * devcgroup_online - initializes devcgroup's behavior and exceptions based on 192 195 * parent's ··· 242 235 if (!dev_cgroup) 243 236 return ERR_PTR(-ENOMEM); 244 237 INIT_LIST_HEAD(&dev_cgroup->exceptions); 238 + INIT_LIST_HEAD(&dev_cgroup->propagate_pending); 245 239 dev_cgroup->behavior = DEVCG_DEFAULT_NONE; 246 240 parent_cgroup = cgroup->parent; 247 241 ··· 421 413 return parent->behavior == DEVCG_DEFAULT_ALLOW; 422 414 } 423 415 416 + /** 417 + * revalidate_active_exceptions - walks through the active exception list and 418 + * revalidates the exceptions based on parent's 419 + * behavior and exceptions. The exceptions that 420 + * are no longer valid will be removed. 421 + * Called with devcgroup_mutex held. 422 + * @devcg: cgroup which exceptions will be checked 423 + * 424 + * This is one of the three key functions for hierarchy implementation. 425 + * This function is responsible for re-evaluating all the cgroup's active 426 + * exceptions due to a parent's exception change. 427 + * Refer to Documentation/cgroups/devices.txt for more details. 428 + */ 429 + static void revalidate_active_exceptions(struct dev_cgroup *devcg) 430 + { 431 + struct dev_exception_item *ex; 432 + struct list_head *this, *tmp; 433 + 434 + list_for_each_safe(this, tmp, &devcg->exceptions) { 435 + ex = container_of(this, struct dev_exception_item, list); 436 + if (!parent_has_perm(devcg, ex)) 437 + dev_exception_rm(devcg, ex); 438 + } 439 + } 440 + 441 + /** 442 + * get_online_devcg - walks the cgroup tree and fills a list with the online 443 + * groups 444 + * @root: cgroup used as starting point 445 + * @online: list that will be filled with online groups 446 + * 447 + * Must be called with devcgroup_mutex held. Grabs RCU lock. 448 + * Because devcgroup_mutex is held, no devcg will become online or offline 449 + * during the tree walk (see devcgroup_online, devcgroup_offline) 450 + * A separated list is needed because propagate_behavior() and 451 + * propagate_exception() need to allocate memory and can block. 452 + */ 453 + static void get_online_devcg(struct cgroup *root, struct list_head *online) 454 + { 455 + struct cgroup *pos; 456 + struct dev_cgroup *devcg; 457 + 458 + lockdep_assert_held(&devcgroup_mutex); 459 + 460 + rcu_read_lock(); 461 + cgroup_for_each_descendant_pre(pos, root) { 462 + devcg = cgroup_to_devcgroup(pos); 463 + if (is_devcg_online(devcg)) 464 + list_add_tail(&devcg->propagate_pending, online); 465 + } 466 + rcu_read_unlock(); 467 + } 468 + 469 + /** 470 + * propagate_exception - propagates a new exception to the children 471 + * @devcg_root: device cgroup that added a new exception 472 + * @ex: new exception to be propagated 473 + * 474 + * returns: 0 in case of success, != 0 in case of error 475 + */ 476 + static int propagate_exception(struct dev_cgroup *devcg_root, 477 + struct dev_exception_item *ex) 478 + { 479 + struct cgroup *root = devcg_root->css.cgroup; 480 + struct dev_cgroup *devcg, *parent, *tmp; 481 + int rc = 0; 482 + LIST_HEAD(pending); 483 + 484 + get_online_devcg(root, &pending); 485 + 486 + list_for_each_entry_safe(devcg, tmp, &pending, propagate_pending) { 487 + parent = cgroup_to_devcgroup(devcg->css.cgroup->parent); 488 + 489 + /* 490 + * in case both root's behavior and devcg is allow, a new 491 + * restriction means adding to the exception list 492 + */ 493 + if (devcg_root->behavior == DEVCG_DEFAULT_ALLOW && 494 + devcg->behavior == DEVCG_DEFAULT_ALLOW) { 495 + rc = dev_exception_add(devcg, ex); 496 + if (rc) 497 + break; 498 + } else { 499 + /* 500 + * in the other possible cases: 501 + * root's behavior: allow, devcg's: deny 502 + * root's behavior: deny, devcg's: deny 503 + * the exception will be removed 504 + */ 505 + dev_exception_rm(devcg, ex); 506 + } 507 + revalidate_active_exceptions(devcg); 508 + 509 + list_del_init(&devcg->propagate_pending); 510 + } 511 + return rc; 512 + } 513 + 514 + static inline bool has_children(struct dev_cgroup *devcgroup) 515 + { 516 + struct cgroup *cgrp = devcgroup->css.cgroup; 517 + 518 + return !list_empty(&cgrp->children); 519 + } 520 + 424 521 /* 425 522 * Modify the exception list using allow/deny rules. 426 523 * CAP_SYS_ADMIN is needed for this. It's at least separate from CAP_MKNOD ··· 562 449 case 'a': 563 450 switch (filetype) { 564 451 case DEVCG_ALLOW: 452 + if (has_children(devcgroup)) 453 + return -EINVAL; 454 + 565 455 if (!may_allow_all(parent)) 566 456 return -EPERM; 567 457 dev_exception_clean(devcgroup); ··· 578 462 return rc; 579 463 break; 580 464 case DEVCG_DENY: 465 + if (has_children(devcgroup)) 466 + return -EINVAL; 467 + 581 468 dev_exception_clean(devcgroup); 582 469 devcgroup->behavior = DEVCG_DEFAULT_DENY; 583 470 break; ··· 675 556 dev_exception_rm(devcgroup, &ex); 676 557 return 0; 677 558 } 678 - return dev_exception_add(devcgroup, &ex); 559 + rc = dev_exception_add(devcgroup, &ex); 560 + break; 679 561 case DEVCG_DENY: 680 562 /* 681 563 * If the default policy is to deny by default, try to remove 682 564 * an matching exception instead. And be silent about it: we 683 565 * don't want to break compatibility 684 566 */ 685 - if (devcgroup->behavior == DEVCG_DEFAULT_DENY) { 567 + if (devcgroup->behavior == DEVCG_DEFAULT_DENY) 686 568 dev_exception_rm(devcgroup, &ex); 687 - return 0; 688 - } 689 - return dev_exception_add(devcgroup, &ex); 569 + else 570 + rc = dev_exception_add(devcgroup, &ex); 571 + 572 + if (rc) 573 + break; 574 + /* we only propagate new restrictions */ 575 + rc = propagate_exception(devcgroup, &ex); 576 + break; 690 577 default: 691 - return -EINVAL; 578 + rc = -EINVAL; 692 579 } 693 - return 0; 580 + return rc; 694 581 } 695 582 696 583 static int devcgroup_access_write(struct cgroup *cgrp, struct cftype *cft,