posix-cpu-timers: Dequeue per-thread timers before exchange_tids()
Subsystems (3)
PATCH v2 1/1 posix-cpu-timers: Dequeue per-thread timers before exchange_tids()
A per-thread CPU timer holds a reference to the PID of the thread it is attached to and, while it is armed, its node is queued in that thread's posix_cputimers. The task is looked up by that PID.
When a non-leader thread exec()s, de_thread() changes which task owns that PID. pid_task(timer->it.cpu.pid, PIDTYPE_PID) then returns NULL, but the node is still queued on tsk, which is alive. timer_lock_sighand() takes a failed lookup to mean that the node is already dequeued, so it has nothing to undo.
begin_new_exec() calls posix_cpu_timers_exit(me) right after exec_task_namespaces() and that removes the leftover node, so the state normally stays invisible. But bprm->point_of_no_return is set before de_thread(), so if unshare_files(), set_mm_exe_file(), exec_mmap() or exec_task_namespaces() fails, the task dies before it gets there. exit_itimers() then frees the k_itimer while its node is still queued, and reaping tsk later erases that freed node from the rbtree.
In short:
the non-leader thread B the parent
timer_create(CLOCK_THREAD_CPUTIME_ID)
timer_settime()
arm_timer() // the node is queued on B
execve()
de_thread(B)
exchange_tids(B, leader) // B's PID now belongs to the leader release_task(leader)
__exit_signal(leader)
posix_cpu_timers_exit(leader) // cleans leader's queue, not B's __unhash_process(leader) // that PID has no task anymore
exec_mmap()
mmap_read_lock_killable(old_mm)
kill(B, SIGKILL)
// -EINTR
get_signal()
do_exit()
exit_itimers()
posix_timer_delete()
posix_cpu_timer_del()
posix_timer_unhash_and_free() // freed while still queued
wait4()
release_task(B)
posix_cpu_timers_exit(B)
cleanup_timerqueue()
timerqueue_del() // use-after-free
KASAN log:
BUG: KASAN: slab-use-after-free in timerqueue_del+0x59/0x80 Write of size 8 at addr ffff88801090e7e0 by task poc/99 ... Call Trace: <TASK> timerqueue_del+0x59/0x80 posix_cpu_timers_exit+0xa8/0xe0 release_task+0x1c6/0xa40 wait_consider_task+0x922/0x15f0 __do_wait+0x3bc/0x3d0 do_wait+0xba/0x1d0 kernel_wait4+0xfe/0x1d0 __do_sys_wait4+0x10b/0x120 do_syscall_64+0xf9/0x540 entry_SYSCALL_64_after_hwframe+0x77/0x7f ... Allocated by task 114: kmem_cache_alloc_noprof+0x143/0x3d0 do_timer_create+0x156/0x890 __x64_sys_timer_create+0x113/0x130 ... Freed by task 104: __kasan_slab_free+0x43/0x70 __rcu_free_sheaf_prepare+0x5c/0x260 rcu_free_sheaf+0x1a/0xc0 rcu_core+0x3d1/0xdf0 handle_softirqs+0x133/0x400 ... The buggy address belongs to the object at ffff88801090e700 which belongs to the cache posix_timers_cache of size 384 The buggy address is located 224 bytes inside of freed 384-byte region [ffff88801090e700, ffff88801090e880)
Dequeue the per-thread CPU timers of tsk before the PID changes hands, so that a failed lookup again implies a dequeued node. Process-wide timers are looked up with PIDTYPE_TGID and transfer_pid() moves that link to tsk, so they are left alone.
---
Changes in v2:
- Add the trigger sequence and the KASAN log to the commit message.
- v1: https://lore.kernel.org/all/anfgrsPlUdwBhdrp@v4bel/
--- fs/exec.c | 12 ++++++++++++ 1 file changed, 12 insertions(+)
fs/exec.c +12 -0
@@ -1003,6 +1003,18 @@ static int de_thread(struct task_struct *tsk)* the former thread group leader: */ +#ifdef CONFIG_POSIX_TIMERS +/* +* exchange_tids() hands this thread's PID to the old leader, +* which is reaped right after. The PID lookup in +* timer_lock_sighand() then fails while the per thread CPU +* timers are still queued here, so dequeue them first. +*/ +spin_lock (lock); +posix_cpu_timers_exit (tsk); +spin_unlock (lock); +#endif +/* Become a process group leader with the old leader's pid. * The old leader becomes a thread of the this thread group. */
Signature · 1 line
2.43.0
OMG, my head spins ;) I'll try to read your explanation tomorrow. Right now I am all confused.
But let me ask some stupid questions right now, please help me to understand the problem.
Quoted 6 lines · Hyunwoo Kim
begin_new_exec()callsposix_cpu_timers_exit(me)right afterexec_task_namespaces()and that removes the leftover node, so the state normally stays invisible. Butbprm->point_of_no_returnis set beforede_thread(), so ifunshare_files(),set_mm_exe_file(),exec_mmap()orexec_task_namespaces()fails, the task dies before it gets there.
Do I understand this correctly? If begin_new_exec() does call posix_cpu_timers_exit(me), then everything is fine.
Yes? If yes
Quoted 7 lines
fs/exec.c +1
-0@@ -1003,6 +1003,18 @@ static int de_thread(struct task_struct *tsk)* the former thread group leader: */ +#ifdef CONFIG_POSIX_TIMERS
fs/exec.c static int de_thread(struct task_struct *tsk) · line 1016
Quoted 10 lines
fs/exec.c +10
-0+/* +* exchange_tids() hands this thread's PID to the old leader, +* which is reaped right after. The PID lookup in +* timer_lock_sighand() then fails while the per thread CPU +* timers are still queued here, so dequeue them first. +*/ +spin_lock (lock); +posix_cpu_timers_exit (tsk); +spin_unlock (lock); +#endif
... then why do we need to call posix_cpu_timers_exit(current) before exchange_tids() ?
And what if another process attaches another cpu timer to the execing thread right after the code above?
Can we move posix_cpu_timers_exit() or the whole CONFIG_POSIX_TIMERS sequence in begin_new_exec() up, right after de_thread() before the "if (retval)" check?
Thank you,
Oleg.
Quoted 8 lines · Oleg Nesterov
OMG, my head spins ;) I'll try to read your explanation tomorrow. Right now I am all confused.
But let me ask some stupid questions right now, please help me to understand the problem.
Quoted 1 line · Hyunwoo Kim
Quoted 8 lines
Quoted 5 lines
begin_new_exec()callsposix_cpu_timers_exit(me)right afterexec_task_namespaces()and that removes the leftover node, so the state normally stays invisible. Butbprm->point_of_no_returnis set beforede_thread(), so ifunshare_files(),set_mm_exe_file(),exec_mmap()orexec_task_namespaces()fails, the task dies before it gets there.Do I understand this correctly? If
begin_new_exec()does callposix_cpu_timers_exit(me), then everything is fine.
Yes. A failure after it, e.g. unshare_sighand(), is harmless.
Quoted 9 lines
Yes? If yes
Quoted 6 lines
fs/exec.c +0
-0@@ -1003,6 +1003,18 @@ static int de_thread(struct task_struct *tsk)* the former thread group leader: */
Quoted 14 lines
fs/exec.c static int de_thread(struct task_struct *tsk) · line 1016
Quoted 11 lines
fs/exec.c +11
-0+#ifdef CONFIG_POSIX_TIMERS +/* +* exchange_tids() hands this thread's PID to the old leader, +* which is reaped right after. The PID lookup in +* timer_lock_sighand() then fails while the per thread CPU +* timers are still queued here, so dequeue them first. +*/ +spin_lock (lock); +posix_cpu_timers_exit (tsk); +spin_unlock (lock); +#endif ... then why do we need to call
posix_cpu_timers_exit(current)beforeexchange_tids()?
Strictly we don't. posix_cpu_timers_exit() walks tsk->posix_cputimers directly and never does the PID lookup, so it only has to happen before exit_itimers()
I put it before exchange_tids() so that the state timer_lock_sighand() warns about never exists in the first place.
Quoted 3 lines
And what if another process attaches another cpu timer to the execing thread right after the code above?
Not for a per-thread timer, pid_for_clock() requires same_thread_group() and there is no other thread left at that point.
A process wide timer has no such check, but it goes to signal->posix_cputimers and the TGID lookup stays valid, so it isn't affected.
Quoted 4 lines
Can we move
posix_cpu_timers_exit()or the wholeCONFIG_POSIX_TIMERSsequence inbegin_new_exec()up, right afterde_thread()before the "if (retval)" check?
posix_cpu_timers_exit(me) there is enough. I haven't tested moving the whole block.
Best regards,
Hyunwoo Kim
Quoted 3 lines · Oleg Nesterov
Can we move
posix_cpu_timers_exit()or the wholeCONFIG_POSIX_TIMERSsequence inbegin_new_exec()up, right afterde_thread()before the "if (retval)" check?
Yes, but that does not buy us anything.
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Oleg Nesterov
Can we move
posix_cpu_timers_exit()or the wholeCONFIG_POSIX_TIMERSsequence inbegin_new_exec()up, right afterde_thread()before the "if (retval)" check?Yes, but that does not buy us anything.
Hmm. I don't understand but this doesn't matter.
I didn't mean this makes sense, I just tried to understand the Hyunwoo's fix, in particular the placement of additional posix_cpu_timers_exit().
Oleg.
Quoted 11 lines · Oleg Nesterov
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Oleg Nesterov
Can we move
posix_cpu_timers_exit()or the wholeCONFIG_POSIX_TIMERSsequence inbegin_new_exec()up, right afterde_thread()before the "if (retval)" check?Yes, but that does not buy us anything.
I didn't mean this makes sense, I just tried to understand the Hyunwoo's fix, in particular the placement of additional
posix_cpu_timers_exit().
Fair enough. But as I was staring at it some more, I thought more about it. Yes, we can do:
ret = de_thread()
if (ret)
goto out;
mop_up_posix_timers();de_thread() only returns failure _before_ the TID swap, so that's safe and spares sprinkling this extra invocation into de_thread().
What a mess.
Quoted 22 lines · Thomas Gleixner
Quoted 11 lines · Oleg Nesterov
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Oleg Nesterov
Can we move
posix_cpu_timers_exit()or the wholeCONFIG_POSIX_TIMERSsequence inbegin_new_exec()up, right afterde_thread()before the "if (retval)" check?Yes, but that does not buy us anything.
I didn't mean this makes sense, I just tried to understand the Hyunwoo's fix, in particular the placement of additional
posix_cpu_timers_exit().Fair enough. But as I was staring at it some more, I thought more about it. Yes, we can do:
ret = de_thread() if (ret) goto out; mop_up_posix_timers();
Quoted 4 lines
de_thread()only returns failure _before_ the TID swap, so that's safe and spares sprinkling this extra invocation intode_thread().What a mess.
I'm still thinking that an indicator for a task doing exec(), which is set _before_ de_thread() and removed afterwards would be more sensible than relying on subtle ordering constraints.
Along with the nonsense to keep timers queued on a task until release_task(). There is no real benefit from keeping them around at after PF_EXITING is set. Sure someone might argue that the precious CPU cycles consumed in do_exit() afterwards won't be accounted, but seriously?
Thanks,
tglx
Quoted 13 lines · Hyunwoo Kim
fs/exec.c +5
-0@@ -1003,6 +1003,18 @@ static int de_thread(struct task_struct *tsk)* the former thread group leader: */ +#ifdef CONFIG_POSIX_TIMERS +/* +* exchange_tids() hands this thread's PID to the old leader, +* which is reaped right after. The PID lookup in +* timer_lock_sighand() then fails while the per thread CPU
fs/exec.c static int de_thread(struct task_struct *tsk) · line 1020
Quoted 10 lines
fs/exec.c +7
-0+* timers are still queued here, so dequeue them first. +*/ +spin_lock (lock); +posix_cpu_timers_exit (tsk); +spin_unlock (lock); +#endif +/* Become a process group leader with the old leader's pid. * The old leader becomes a thread of the this thread group. */
Similar to the other issue vs. POSIX timer signals this really wants to be solved in exit() independent of non-leader exec(). Let me have a look.
Quoted 20 lines · Thomas Gleixner
Quoted 19 lines · Hyunwoo Kim
fs/exec.c +11
-0@@ -1003,6 +1003,18 @@ static int de_thread(struct task_struct *tsk)* the former thread group leader: */ +#ifdef CONFIG_POSIX_TIMERS +/* +* exchange_tids() hands this thread's PID to the old leader, +* which is reaped right after. The PID lookup in +* timer_lock_sighand() then fails while the per thread CPU +* timers are still queued here, so dequeue them first. +*/ +spin_lock (lock); +posix_cpu_timers_exit (tsk); +spin_unlock (lock); +#endif
Quoted 8 lines
fs/exec.c static int de_thread(struct task_struct *tsk) · line 1020
Quoted 4 lines
fs/exec.c +1
-0+/* Become a process group leader with the old leader's pid. * The old leader becomes a thread of the this thread group. */ Similar to the other issue vs. POSIX timer signals this really wants to be solved in
exit()independent of non-leaderexec(). Let me have a look.
Duh. That's about the non-leader itself....
Why did anyone think that swapping TIDs is a brilliant idea?
I still think that this should be done differently. There is no point in doing the disarming in release_task(). Let me think some more.