cgroup: fix race between fork and cgroup.kill
PATCH 1/1 cgroup: fix race between fork and cgroup.kill
Tejun reported the following race between fork() and cgroup.kill at [1].
Tejun:
I was looking at cgroup.kill implementation and wondering whether there could be a race window. So, __cgroup_kill() does the following:
k1. Set CGRP_KILL.
k2. Iterate tasks and deliver SIGKILL.
k3. Clear CGRP_KILL.
The copy_process() does the following:
c1. Copy a bunch of stuff.
c2. Grab siglock.
c3. Check fatal_signal_pending().
c4. Commit to forking.
c5. Release siglock.
c6. Call cgroup_post_fork() which puts the task on the css_set and tests
CGRP_KILL.
The intention seems to be that either a forking task gets SIGKILL and terminates on c3 or it sees CGRP_KILL on c6 and kills the child. However, I don't see what guarantees that k3 can't happen before c6. ie. After a forking task passes c5, k2 can take place and then before the forking task reaches c6, k3 can happen. Then, nobody would send SIGKILL to the child. What am I missing?
This is indeed a race. One way to fix this race is by taking cgroup_threadgroup_rwsem in write mode in __cgroup_kill() as the fork() side takes cgroup_threadgroup_rwsem in read mode from cgroup_can_fork() to cgroup_post_fork(). However that would be heavy handed as this adds one more potential stall scenario for cgroup.kill which is usually called under extreme situation like memory pressure.
To fix this race, let's maintain a sequence number per cgroup which gets incremented on __cgroup_kill() call. On the fork() side, the cgroup_can_fork() will cache the sequence number locally and recheck it against the cgroup's sequence number at cgroup_post_fork() site. If the sequence numbers mismatch, it means __cgroup_kill() can been called and we should send SIGKILL to the newly created task.
--- include/linux/cgroup-defs.h | 6 +++--- include/linux/sched/task.h | 1 + kernel/cgroup/cgroup.c | 20 ++++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-)
include/linux/cgroup-defs.h +3 -3
@@ -71,9 +71,6 @@ enum {/* Cgroup is frozen. */ CGRP_FROZEN, - -/* Control group has to be killed. */ - CGRP_KILL, };/* cgroup_root->flags */
@@ -461,6 +458,9 @@ struct cgroup {int nr_threaded_children;/* # of live threaded child cgroups */ +/* sequence number for cgroup.kill, serialized by css_set_lock. */ +unsigned int kill_seq; +struct kernfs_node *kn;/* cgroup kernfs entry */ struct cgroup_file procs_file;/* handle for "cgroup.procs" */ struct cgroup_file events_file;/* handle for "cgroup.events" */
include/linux/sched/task.h +1 -0
@@ -43,6 +43,7 @@ struct kernel_clone_args {void *fn_arg;struct cgroup *cgrp;struct css_set *cset; +unsigned int kill_seq; };/*
kernel/cgroup/cgroup.c +12 -8
@@ -4013,7 +4013,7 @@ static void __cgroup_kill(struct cgroup *cgrp)lockdep_assert_held (&cgroup_mutex);spin_lock_irq (&css_set_lock); -set_bit (CGRP_KILL , &cgrp->flags); + cgrp->kill_seq++;spin_unlock_irq (&css_set_lock);css_task_iter_start (&cgrp->self,CSS_TASK_ITER_PROCS |CSS_TASK_ITER_THREADED , &it);
@@ -4029,10 +4029,6 @@ static void __cgroup_kill(struct cgroup *cgrp)send_sig (SIGKILL , task,0 ); }css_task_iter_end (&it); - -spin_lock_irq (&css_set_lock); -clear_bit (CGRP_KILL , &cgrp->flags); -spin_unlock_irq (&css_set_lock); }static void cgroup_kill (struct cgroup *cgrp)
@@ -6488,6 +6484,10 @@ static int cgroup_css_set_fork(struct kernel_clone_args *kargs)spin_lock_irq (&css_set_lock); cset =task_css_set (current);get_css_set (cset); +if (kargs->cgrp) + kargs->kill_seq = kargs->cgrp->kill_seq; +else + kargs->kill_seq = cset->dfl_cgrp->kill_seq;spin_unlock_irq (&css_set_lock);if (!(kargs->flags &CLONE_INTO_CGROUP )) {
@@ -6668,6 +6668,7 @@ void cgroup_post_fork(struct task_struct *child, struct kernel_clone_args *kargs) __releases(&cgroup_threadgroup_rwsem) __releases(&cgroup_mutex) { + unsigned int cgrp_kill_seq = 0; unsigned long cgrp_flags = 0; bool kill = false; struct cgroup_subsys *ss;
@@ -6681,10 +6682,13 @@ void cgroup_post_fork(struct task_struct *child,/* init tasks are special, only link regular threads */ if (likely (child->pid)) { -if (kargs->cgrp) +if (kargs->cgrp) { cgrp_flags = kargs->cgrp->flags; -else + cgrp_kill_seq = kargs->cgrp->kill_seq; + }else { cgrp_flags = cset->dfl_cgrp->flags; + cgrp_kill_seq = cset->dfl_cgrp->kill_seq; + }WARN_ON_ONCE (!list_empty (&child->cg_list)); cset->nr_tasks++;
@@ -6719,7 +6723,7 @@ void cgroup_post_fork(struct task_struct *child,* child down right after we finished preparing it for * userspace. */ - kill =test_bit (CGRP_KILL , &cgrp_flags); + kill = kargs->kill_seq != cgrp_kill_seq; }spin_unlock_irq (&css_set_lock);
Signature · 1 line
2.43.5
kernel/cgroup/cgroup.c void cgroup_post_fork(struct task_struct *child, · line 6671
Quoted 5 lines · Shakeel Butt
kernel/cgroup/cgroup.c +1
-0@@ -6668,6 +6668,7 @@ void cgroup_post_fork(struct task_struct *child, struct kernel_clone_args *kargs) __releases(&cgroup_threadgroup_rwsem) __releases(&cgroup_mutex) { + unsigned int cgrp_kill_seq = 0;
This initialization is only needed for the extra "init tasks" branch (the value should equal to whatever init_css_set.dfl_cgroup has, i.e. 0).
kernel/cgroup/cgroup.c void cgroup_post_fork(struct task_struct *child, · line 6688
Quoted 13 lines
kernel/cgroup/cgroup.c +3
-2unsigned long cgrp_flags =0 ;bool kill = false;struct cgroup_subsys *ss;@@ -6681,10 +6682,13 @@ void cgroup_post_fork(struct task_struct *child,/* init tasks are special, only link regular threads */ if (likely (child->pid)) { -if (kargs->cgrp) +if (kargs->cgrp) { cgrp_flags = kargs->cgrp->flags; -else + cgrp_kill_seq = kargs->cgrp->kill_seq; + }else {
This should not be strictly necessary thanks to cgroup_mutex during cgroup_kill and CLONE_INTO_CGROUP.
Quoted 7 lines
kernel/cgroup/cgroup.c +2
-0cgrp_flags = cset->dfl_cgrp->flags; + cgrp_kill_seq = cset->dfl_cgrp->kill_seq; + }WARN_ON_ONCE (!list_empty (&child->cg_list)); cset->nr_tasks++;
kernel/cgroup/cgroup.c void cgroup_post_fork(struct task_struct *child, · line 6729
Quoted 8 lines
kernel/cgroup/cgroup.c +1
-1@@ -6719,7 +6723,7 @@ void cgroup_post_fork(struct task_struct *child,* child down right after we finished preparing it for * userspace. */ - kill =test_bit (CGRP_KILL , &cgrp_flags); + kill = kargs->kill_seq != cgrp_kill_seq; }spin_unlock_irq (&css_set_lock);
The above are only notes for better understanding, I don't think the fix needs changes in that regard. Thanks
Quoted 41 lines · Shakeel Butt
Tejun reported the following race between
fork()and cgroup.kill at [1].Tejun:
I was looking at cgroup.kill implementation and wondering whether there could be a race window. So,__cgroup_kill()does the following:k1. Set
CGRP_KILL.
k2. Iterate tasks and deliver SIGKILL.
k3. ClearCGRP_KILL.The
copy_process()does the following:c1. Copy a bunch of stuff.
c2. Grab siglock.
c3. Checkfatal_signal_pending().
c4. Commit to forking.
c5. Release siglock.
c6. Callcgroup_post_fork()which puts the task on thecss_setand tests
CGRP_KILL.The intention seems to be that either a forking task gets SIGKILL and terminates on c3 or it sees
CGRP_KILLon c6 and kills the child. However, I don't see what guarantees that k3 can't happen before c6. ie. After a forking task passes c5, k2 can take place and then before the forking task reaches c6, k3 can happen. Then, nobody would send SIGKILL to the child. What am I missing?This is indeed a race. One way to fix this race is by taking
cgroup_threadgroup_rwsemin write mode in__cgroup_kill()as thefork()side takescgroup_threadgroup_rwsemin read mode fromcgroup_can_fork()tocgroup_post_fork(). However that would be heavy handed as this adds one more potential stall scenario for cgroup.kill which is usually called under extreme situation like memory pressure.To fix this race, let's maintain a sequence number per cgroup which gets incremented on
__cgroup_kill()call. On thefork()side, thecgroup_can_fork()will cache the sequence number locally and recheck it against the cgroup's sequence number atcgroup_post_fork()site. If the sequence numbers mismatch, it means__cgroup_kill()can been called and we should send SIGKILL to the newly created task.
Quoted 4 lines
Reported-by Tejun Heo <[email protected]>Fixes 661ee6280931 ("cgroup: introduce cgroup.kill")Signed-off-by Shakeel Butt <[email protected]>
Applied to cgroup/for-6.14-fixes.
Thanks.
Signature · 1 line
tejun
Quoted 41 lines · Shakeel Butt
Tejun reported the following race between
fork()and cgroup.kill at [1].Tejun:
I was looking at cgroup.kill implementation and wondering whether there could be a race window. So,__cgroup_kill()does the following:k1. Set
CGRP_KILL.
k2. Iterate tasks and deliver SIGKILL.
k3. ClearCGRP_KILL.The
copy_process()does the following:c1. Copy a bunch of stuff.
c2. Grab siglock.
c3. Checkfatal_signal_pending().
c4. Commit to forking.
c5. Release siglock.
c6. Callcgroup_post_fork()which puts the task on thecss_setand tests
CGRP_KILL.The intention seems to be that either a forking task gets SIGKILL and terminates on c3 or it sees
CGRP_KILLon c6 and kills the child. However, I don't see what guarantees that k3 can't happen before c6. ie. After a forking task passes c5, k2 can take place and then before the forking task reaches c6, k3 can happen. Then, nobody would send SIGKILL to the child. What am I missing?This is indeed a race. One way to fix this race is by taking
cgroup_threadgroup_rwsemin write mode in__cgroup_kill()as thefork()side takescgroup_threadgroup_rwsemin read mode fromcgroup_can_fork()tocgroup_post_fork(). However that would be heavy handed as this adds one more potential stall scenario for cgroup.kill which is usually called under extreme situation like memory pressure.To fix this race, let's maintain a sequence number per cgroup which gets incremented on
__cgroup_kill()call. On thefork()side, thecgroup_can_fork()will cache the sequence number locally and recheck it against the cgroup's sequence number atcgroup_post_fork()site. If the sequence numbers mismatch, it means__cgroup_kill()can been called and we should send SIGKILL to the newly created task.
Quoted 5 lines
Reported-by Tejun Heo <[email protected]>Fixes 661ee6280931 ("cgroup: introduce cgroup.kill")Signed-off-by Shakeel Butt <[email protected]>---