Skip to contentSkip to conversation

posix-cpu-timers: Dequeue per-thread timers before exchange_tids()

PATCH In review Hyunwoo Kim 3 participants 9 messages Aug 26 – Sep 1

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

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.

Fixes 55e8c8eb2c7b ("posix-cpu-timers: Store a reference to a pid not a task")

---
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
index 745f6eb5279e6..9b2d140bd9f2a 100644
@@ -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
#1 lore raw

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() 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.

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.

#2 lore raw
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() 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.

Do I understand this correctly? If begin_new_exec() does call posix_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) before exchange_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 whole CONFIG_POSIX_TIMERS sequence in begin_new_exec() up, right after de_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

#3 lore raw
In reply to Oleg Nesterov
Quoted 3 lines · Oleg Nesterov

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?

Yes, but that does not buy us anything.

#4 lore raw
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Oleg Nesterov

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?

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.

#5 lore raw
Quoted 11 lines · Oleg Nesterov
Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · Oleg Nesterov

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?

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.

#6 lore raw
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 whole CONFIG_POSIX_TIMERS sequence in begin_new_exec() up, right after de_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 into de_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

#7 lore raw
In reply to Hyunwoo Kim
Quoted 13 lines · Hyunwoo Kim
fs/exec.c +5 -0
index 745f6eb5279e6..9b2d140bd9f2a 100644
@@ -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.

#8 lore raw
Quoted 20 lines · Thomas Gleixner
Quoted 19 lines · Hyunwoo Kim
fs/exec.c +11 -0
index 745f6eb5279e6..9b2d140bd9f2a 100644
@@ -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-leader exec(). 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.