Skip to contentSkip to conversation

exec/exit: POSIX timer related bugfixes and related cleanups

PATCH Applied Thomas Gleixner 6 participants 31 messages Sep 11 – Sep 16

Subsystems (4)
lore
Share & tools
lore

Thomas Gleixner

author maintainer
#0 lore raw

v3 cover exec/exit: POSIX timer related bugfixes and related cleanups

This is a follow up to V2 which can be found here:

https://lore.kernel.org/[email protected]

Recent findings from Hyunwoo unearthed two bugs in handling POSIX timers on exec().

The relevant patches, reports and discussions can be found here:

https://patch.msgid.link/aok1rdkBgZsynHZB@v4bel
https://patch.msgid.link/ao7Q8miiuLAPVnWv@v4bel

TLDR:

Both problems are related to non-leader exec(). POSIX CPU timers which are targeted at tasks hold a pid reference of the target task, which is used to look up the task in the related POSIX timer operations.

The non-leader exec() switches the TID of the old and the new leader, which obviously invalidates these references for pid_task(PIDTYPE_PID) lookups.

This causes UAFs due to the resulting list corruptions or premature freeing without removing the underlying POSIX CPU timers from the involved tasks.

The first issue which corrupts the signal pending list is solved by:

- Preventing the queueing of per task signals on a task which has PF_EXITING set.

- Protecting the unlocked setting of PF_EXITING in exit_signals() with sighand lock.

- Flushing all per task signals right in exit_signals()

The second issue which keeps the POSIX CPU timers queued on the new leader is solved by:

- Moving the exec related POSIX timer cleanup right after de_thread() which ensures that the timers queued in new_leader::posix_cputimers are removed before the underlying POSIX timers are deleted.

After looking deeper at the exit() handling it turned out that the POSIX timer cleanups can be done early in do_exit() instead of delaying them until release_task().

The reason for this late cleanup is that POSIX CPU timers can be created, rearmed and deleted as long as a task is visible, i.e. the pid is hashed and sighand is not NULL. This allows to retrieve information from the timer up to the point where the task is gone for real and that can't be changed easily as that'd be a user visible change.

But once PF_EXITING is set on a task the task does not longer expire POSIX CPU timers. So it makes no sense that the timers stay queued in task::posix_cputimers after that point.

The only thing which needs to be prevented is that timers are requeued on task::posix_cputimers once PF_EXITING is set or requeued on signal::posix_cputimers when PF_EXITING is set and signal::live is zero, which indicates that the thread group is dead.

With that solved the timers can be dequeued from task::posix_cputimer pending when a task exits and from signal::posix_cputimer pending once the threadgroup reaches the dead state, i.e. signal::live goes to zero in do_exit().

The changes vs. V2:

- Document in the change log that the non-leader exec() case is properly ordered against the lockless pending list flush. - Frederic, Peter, Alan

- Add a comment to document when a lockless flush is safe

- Cancel pending POSIX CPU timer work only for exit(), not for exec() - Oleg

- Pick up Reviewed-by tags where appropriate

The delta patch against V2 is below.

The series applies on 7.3-rc1 and is avalaible from git:

git://git.kernel.org/pub/scm/linux/kernel/git/tglx/devel.git posix-timers

Thanks,

tglx
---

kernel/signal.c +14 -0
index f93d8f77ec1a..7081819eddd1 100644
@@ -457,6 +457,20 @@ static void __sigqueue_free(struct sigqueue *q)
        kmem_cache_free(sigqueue_cachep, q);
 }
 
+/*
+ * flush_sigqueue_list() can only be invoked without holding sighand::siglock in
+ * the following cases:
+ *
+ *  1) When flushing task::pending _after_ setting task::flags PF_EXITING
+ *
+ *     All functions which try to send a signal to @task will observe PF_EXITING
+ *     and drop the signal.
+ *
+ *  2) When flushing task::signal::shared_pending _after_ the last task in a
+ *     thread group was unhashed and task::sighand is NULL.
+ *
+ *     Nothing can queue a signal anymore because sighand is NULL.
+ */
 static void flush_sigqueue_list(struct list_head *head)
 {
        struct sigqueue *q, *tmp;
kernel/time/posix-cpu-timers.c +3 -0
index 53e47c1b56c2..9f90500894dc 100644
@@ -1270,6 +1270,9 @@ static void posix_cpu_timers_work(struct callback_head *work)
 
 static inline void posix_cpu_timers_exit_work(void)
 {
+       /* Canceling the work is only valid for exit() but not for exec() */
+       if (!(current->flags & PF_EXITING))
+               return;
        /*
         * current->flags has PF_EXITING set so this can be done lockless and
         * with interrupts enabled as PF_EXITING prevents the interrupt from
author maintainer
#1 lore raw

PATCH v3 1/8 signal: Prevent exec() race

From: Thomas Gleixner <[email protected]>

Hyunwoo debugged the following KASAN UAF splat:

  BUG: KASAN: slab-use-after-free in __send_signal_locked+0xb27/0xba0
  Write of size 8 at addr ffff888007ed80c8 by task poc/79
  ...
  Call Trace:
   __send_signal_locked+0xb27/0xba0
   do_send_sig_info+0xa7/0x160
   do_send_specific+0x76/0xa0
   __x64_sys_tgkill+0x193/0x270
  ...
  Allocated by task 80:
   do_timer_create+0x1a4/0x1030
   __x64_sys_timer_create+0x145/0x190
  ...
  Freed by task 12:
   kmem_cache_free_bulk+0x1f8/0x4a0
   kvfree_rcu_bulk+0x14f/0x1c0
   kfree_rcu_work+0x128/0x1a0
  ...
  Last potentially related work creation:
   kvfree_call_rcu+0x39/0x390
   __flush_itimer_signals+0x211/0x320
   flush_itimer_signals+0x47/0x90
   begin_new_exec+0xa6b/0x28c0

It turned out that this happens with a non-leader exec() as Hyunwoo explained:

de_thread() calls exchange_tids() before release_task(leader), so the struct pid held by a SIGEV_THREAD_ID timer created against the leader's tid now points to the thread which called execve(). pid_task() returns that thread and lock_task_sighand() on it succeeds.

If the timer signal is blocked, its sigqueue stays queued on the leader's task::pending. The next expiry of that timer can then run while release_task() flushes the queue.

posixtimer_send_sigqueue() checks whether the sigqueue is already queued with a plain list_empty(), which only reads list_head::next. list_del_init() is not atomic and INIT_LIST_HEAD() stores list_head::next before list_head::prev, so the check can pass in between. list_add_tail() queues the entry on the task::pending of the live thread, and the list_head::prev store from the flush then overwrites the list_head::prev link that list_add_tail() has just set.

__flush_itimer_signals() does not undo that either. With list_head::prev pointing at the entry itself, its list_del_init() only stores the same values again, so the entry is not removed from the list. It is still there after the last reference is dropped and the timer is freed by RCU, and the list_add_tail() of a later tgkill() follows that list_head::prev into the freed timer.

This problem surfaced with the recent commit which moved the sigqueue flush out of the sighand lock held region.

Hyonwoo proposed to fix this by using list_del_init_careful(), but that just papers over the problem. After some disucssions and various attempts to solve it, Eric pointed out that there is no reason to flush task::pending late in release_task() and it should be done in exit_signals() already.

As nothing can collect and deliver signals which are queued in a dying task's pending queue, there is no reason to delay it further.

But it has to be ensured that no signals can be queued into it after that point. exit_signals() sets PF_EXITING in task::flags, which can be used as an indicator for this.

Cure it by:

- Preventing signal queueing for task private signals (PIDTYPE_PID) when the task has PF_EXITING set in __send_signal_locked() and in posixtimer_send_sigqueue().

- Protecting the unlocked setting of PF_EXITING in exit_signals() for the task group empty and the group exit case with sighand lock

- Flushing task::pending signals right there.

Optimize that by moving the whole pending list to an on-stack list head under sighand lock and free the signals without the lock held.

There has been quite some discussion about the lockless flush and the non-leader exec case on weakly ordered systems. The problem is that a third party which tries to send a posix timer signal relies on the PID lookup to find the target task and that lookup might result in the new leader when the signal was originaly directed to the old leader. In case that the signal was queued on the old leader then the lockless flush raised a concern over the following situation:

old_leader new_leader third party

A: flush_list() // list_del_init() stores to sigqueue

LOCK (tasklist)
old_leader->exit_state = EXIT_ZOMBIE;
B: UNLOCK (tasklist)

C: LOCK (tasklist)

			if (old_leader->exit_state)
			   transfer_tids()
D:			     store PID
						posix_timer_send_sigqueue()
						// Observes #D so t = new_leader

E: t = get_target()

F: LOCK (sighand)

G: if (list_empty(sigqueue))
list_add(sigqueue)

The concern was that the third party might observe #D but not observe #A and therefore would proceed to #G while the list_del() stores (#A) in flush_list() are not visible yet, which could result in list corruption.

That would be possible if looking at it solely from a RELEASE+ACQUIRE ordering point of view, but B-C is a UNLOCK+LOCK hand-over, which is not the same as RELEASE+ACQUIRE:

RELEASE+ACQUIRE: RCpc, only the CPUs involved agree on the ordering
UNLOCK+LOCK: RCtso, the hand-over is store-ordering

As B-C is UNLOCK+LOCK, which is RCtso and that does impose store order, A stores must happen before the D store.

Combine with E-F, which has a data dependency from the LOAD to the LOCK and thereby constraints later LOADs, those sigqueue loads in G that come after F must in fact observe the A stores.

Fixes fb3bbcfe344e ("exit: change the release_task() paths to call flush_sigqueue() lockless")

---
V5: Amend change log to document that the lockless flush is safe - Oleg, Frederic, Peter
Add comments to flush_sigqueue_list() when lockless is safe

V4: Prevent requeuing of signals which are unignored - Oleg, Eric

Split out the decision into an inline which can be reused by the posix CPU timer follow up changes.

V3: Restructure code and fix the missing unlock - Oleg

V2: Don't flush w/o sighand lock held - Oleg
Move the pending list under the lock and free it lockless

---
 kernel/exit.c   |   11 ++---
 kernel/signal.c |  117 +++++++++++++++++++++++++++++++++++++++++---------------
 2 files changed, 92 insertions(+), 36 deletions(-)
kernel/exit.c +6 -5
@@ -299,12 +299,13 @@ void release_task(struct task_struct *p)
 	free_pids(post.pids);
 	release_thread(p);
 	/*
-	 * This task was already removed from the process/thread/pid lists
-	 * and lock_task_sighand(p) can't succeed. Nobody else can touch
-	 * ->pending or, if group dead, signal->shared_pending. We can call
-	 * flush_sigqueue() lockless.
+	 * This task was already removed from the process/thread/pid lists and
+	 * lock_task_sighand(p) can't succeed. If it's the group leader then
+	 * flush tsk->signal->shared_pending. tsk->pending has been flushed
+	 * already in exit_signals(). Nothing else can touch
+	 * signal->shared_pending anymore, so flush_sigqueue() can be invoked
+	 * lockless.
 	 */
-	flush_sigqueue(&p->pending);
 	if (thread_group_leader(p))
 		flush_sigqueue(&p->signal->shared_pending);
 
kernel/signal.c +86 -31

188 lines not on this page · raw message

maintainer Reviewed-by on v3 1/8
#2 lore raw

Re: [patch V3 1/8] signal: Prevent exec() race

Quoted 132 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Hyunwoo debugged the following KASAN UAF splat:

  BUG: KASAN: slab-use-after-free in __send_signal_locked+0xb27/0xba0
  Write of size 8 at addr ffff888007ed80c8 by task poc/79
  ...
  Call Trace:
   __send_signal_locked+0xb27/0xba0
   do_send_sig_info+0xa7/0x160
   do_send_specific+0x76/0xa0
   __x64_sys_tgkill+0x193/0x270
  ...
  Allocated by task 80:
   do_timer_create+0x1a4/0x1030
   __x64_sys_timer_create+0x145/0x190
  ...
  Freed by task 12:
   kmem_cache_free_bulk+0x1f8/0x4a0
   kvfree_rcu_bulk+0x14f/0x1c0
   kfree_rcu_work+0x128/0x1a0
  ...
  Last potentially related work creation:
   kvfree_call_rcu+0x39/0x390
   __flush_itimer_signals+0x211/0x320
   flush_itimer_signals+0x47/0x90
   begin_new_exec+0xa6b/0x28c0

It turned out that this happens with a non-leader exec() as Hyunwoo explained:

de_thread() calls exchange_tids() before release_task(leader), so the struct pid held by a SIGEV_THREAD_ID timer created against the leader's tid now points to the thread which called execve(). pid_task() returns that thread and lock_task_sighand() on it succeeds.

If the timer signal is blocked, its sigqueue stays queued on the leader's task::pending. The next expiry of that timer can then run while release_task() flushes the queue.

posixtimer_send_sigqueue() checks whether the sigqueue is already queued with a plain list_empty(), which only reads list_head::next. list_del_init() is not atomic and INIT_LIST_HEAD() stores list_head::next before list_head::prev, so the check can pass in between. list_add_tail() queues the entry on the task::pending of the live thread, and the list_head::prev store from the flush then overwrites the list_head::prev link that list_add_tail() has just set.

__flush_itimer_signals() does not undo that either. With list_head::prev pointing at the entry itself, its list_del_init() only stores the same values again, so the entry is not removed from the list. It is still there after the last reference is dropped and the timer is freed by RCU, and the list_add_tail() of a later tgkill() follows that list_head::prev into the freed timer.

This problem surfaced with the recent commit which moved the sigqueue flush out of the sighand lock held region.

Hyonwoo proposed to fix this by using list_del_init_careful(), but that just papers over the problem. After some disucssions and various attempts to solve it, Eric pointed out that there is no reason to flush task::pending late in release_task() and it should be done in exit_signals() already.

As nothing can collect and deliver signals which are queued in a dying task's pending queue, there is no reason to delay it further.

But it has to be ensured that no signals can be queued into it after that point. exit_signals() sets PF_EXITING in task::flags, which can be used as an indicator for this.

Cure it by:

- Preventing signal queueing for task private signals (PIDTYPE_PID) when the task has PF_EXITING set in __send_signal_locked() and in posixtimer_send_sigqueue().

- Protecting the unlocked setting of PF_EXITING in exit_signals() for the task group empty and the group exit case with sighand lock

- Flushing task::pending signals right there.

Optimize that by moving the whole pending list to an on-stack list head under sighand lock and free the signals without the lock held.

There has been quite some discussion about the lockless flush and the non-leader exec case on weakly ordered systems. The problem is that a third party which tries to send a posix timer signal relies on the PID lookup to find the target task and that lookup might result in the new leader when the signal was originaly directed to the old leader. In case that the signal was queued on the old leader then the lockless flush raised a concern over the following situation:

old_leader new_leader third party

A: flush_list() // list_del_init() stores to sigqueue

   LOCK (tasklist)
   old_leader->exit_state = EXIT_ZOMBIE;

B: UNLOCK (tasklist)

C: LOCK (tasklist)

			if (old_leader->exit_state)
			   transfer_tids()
D:			     store PID
						posix_timer_send_sigqueue()
						// Observes #D so t = new_leader

E: t = get_target()

F: LOCK (sighand)

G: if (list_empty(sigqueue))
list_add(sigqueue)

The concern was that the third party might observe #D but not observe #A and therefore would proceed to #G while the list_del() stores (#A) in flush_list() are not visible yet, which could result in list corruption.

That would be possible if looking at it solely from a RELEASE+ACQUIRE ordering point of view, but B-C is a UNLOCK+LOCK hand-over, which is not the same as RELEASE+ACQUIRE:

RELEASE+ACQUIRE: RCpc, only the CPUs involved agree on the ordering
UNLOCK+LOCK: RCtso, the hand-over is store-ordering

As B-C is UNLOCK+LOCK, which is RCtso and that does impose store order, A stores must happen before the D store.

Combine with E-F, which has a data dependency from the LOAD to the LOCK and thereby constraints later LOADs, those sigqueue loads in G that come after F must in fact observe the A stores.

Quoted 8 lines
Fixes fb3bbcfe344e ("exit: change the release_task() paths to call flush_sigqueue() lockless")
Signature · 2 lines
Frederic Weisbecker
SUSE Labs
#3 lore raw

[tip: timers/core] signal: Prevent exec() race

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     d2710c8d938ae6a825a6463158e6e6f31eac792a
Gitweb:        https://git.kernel.org/tip/d2710c8d938ae6a825a6463158e6e6f31eac792a
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:12 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:03 +02:00

signal: Prevent exec() race

Hyunwoo debugged the following KASAN UAF splat:

  BUG: KASAN: slab-use-after-free in __send_signal_locked+0xb27/0xba0
  Write of size 8 at addr ffff888007ed80c8 by task poc/79
  ...
  Call Trace:
   __send_signal_locked+0xb27/0xba0
   do_send_sig_info+0xa7/0x160
   do_send_specific+0x76/0xa0
   __x64_sys_tgkill+0x193/0x270
  ...
  Allocated by task 80:
   do_timer_create+0x1a4/0x1030
   __x64_sys_timer_create+0x145/0x190
  ...
  Freed by task 12:
   kmem_cache_free_bulk+0x1f8/0x4a0
   kvfree_rcu_bulk+0x14f/0x1c0
   kfree_rcu_work+0x128/0x1a0
  ...
  Last potentially related work creation:
   kvfree_call_rcu+0x39/0x390
   __flush_itimer_signals+0x211/0x320
   flush_itimer_signals+0x47/0x90
   begin_new_exec+0xa6b/0x28c0

It turned out that this happens with a non-leader exec() as Hyunwoo explained:

de_thread() calls exchange_tids() before release_task(leader), so the struct pid held by a SIGEV_THREAD_ID timer created against the leader's tid now points to the thread which called execve(). pid_task() returns that thread and lock_task_sighand() on it succeeds.

If the timer signal is blocked, its sigqueue stays queued on the leader's task::pending. The next expiry of that timer can then run while release_task() flushes the queue.

posixtimer_send_sigqueue() checks whether the sigqueue is already queued with a plain list_empty(), which only reads list_head::next. list_del_init() is not atomic and INIT_LIST_HEAD() stores list_head::next before list_head::prev, so the check can pass in between. list_add_tail() queues the entry on the task::pending of the live thread, and the list_head::prev store from the flush then overwrites the list_head::prev link that list_add_tail() has just set.

__flush_itimer_signals() does not undo that either. With list_head::prev pointing at the entry itself, its list_del_init() only stores the same values again, so the entry is not removed from the list. It is still there after the last reference is dropped and the timer is freed by RCU, and the list_add_tail() of a later tgkill() follows that list_head::prev into the freed timer.

This problem surfaced with the recent commit which moved the sigqueue flush out of the sighand lock held region.

Hyonwoo proposed to fix this by using list_del_init_careful(), but that just papers over the problem. After some disucssions and various attempts to solve it, Eric pointed out that there is no reason to flush task::pending late in release_task() and it should be done in exit_signals() already.

As nothing can collect and deliver signals which are queued in a dying task's pending queue, there is no reason to delay it further.

But it has to be ensured that no signals can be queued into it after that point. exit_signals() sets PF_EXITING in task::flags, which can be used as an indicator for this.

Cure it by:

- Preventing signal queueing for task private signals (PIDTYPE_PID) when the task has PF_EXITING set in __send_signal_locked() and in posixtimer_send_sigqueue().

- Protecting the unlocked setting of PF_EXITING in exit_signals() for the task group empty and the group exit case with sighand lock

- Flushing task::pending signals right there.

Optimize that by moving the whole pending list to an on-stack list head under sighand lock and free the signals without the lock held.

There has been quite some discussion about the lockless flush and the non-leader exec case on weakly ordered systems. The problem is that a third party which tries to send a posix timer signal relies on the PID lookup to find the target task and that lookup might result in the new leader when the signal was originaly directed to the old leader. In case that the signal was queued on the old leader then the lockless flush raised a concern over the following situation:

old_leader new_leader third party

A: flush_list() // list_del_init() stores to sigqueue

LOCK (tasklist)
old_leader->exit_state = EXIT_ZOMBIE;
B: UNLOCK (tasklist)

C: LOCK (tasklist)

			if (old_leader->exit_state)
			   transfer_tids()
D:			     store PID
						posix_timer_send_sigqueue()
						// Observes #D so t = new_leader

E: t = get_target()

F: LOCK (sighand)

G: if (list_empty(sigqueue))
list_add(sigqueue)

The concern was that the third party might observe #D but not observe #A and therefore would proceed to #G while the list_del() stores (#A) in flush_list() are not visible yet, which could result in list corruption.

That would be possible if looking at it solely from a RELEASE+ACQUIRE ordering point of view, but B-C is a UNLOCK+LOCK hand-over, which is not the same as RELEASE+ACQUIRE:

RELEASE+ACQUIRE: RCpc, only the CPUs involved agree on the ordering
UNLOCK+LOCK: RCtso, the hand-over is store-ordering

As B-C is UNLOCK+LOCK, which is RCtso and that does impose store order, A stores must happen before the D store.

Combine with E-F, which has a data dependency from the LOAD to the LOCK and thereby constraints later LOADs, those sigqueue loads in G that come after F must in fact observe the A stores.

---
 kernel/exit.c   |  11 ++--
 kernel/signal.c | 115 ++++++++++++++++++++++++++++++++++-------------
 2 files changed, 91 insertions(+), 35 deletions(-)
kernel/exit.c +6 -5
index 4e028f1..424c44a 100644
@@ -302,12 +302,13 @@ repeat:
 	free_pids(post.pids);
 	release_thread(p);
 	/*
-	 * This task was already removed from the process/thread/pid lists
-	 * and lock_task_sighand(p) can't succeed. Nobody else can touch
-	 * ->pending or, if group dead, signal->shared_pending. We can call
-	 * flush_sigqueue() lockless.
+	 * This task was already removed from the process/thread/pid lists and
+	 * lock_task_sighand(p) can't succeed. If it's the group leader then
+	 * flush tsk->signal->shared_pending. tsk->pending has been flushed
+	 * already in exit_signals(). Nothing else can touch
+	 * signal->shared_pending anymore, so flush_sigqueue() can be invoked
+	 * lockless.
 	 */
-	flush_sigqueue(&p->pending);
 	if (thread_group_leader(p))
 		flush_sigqueue(&p->signal->shared_pending);
 
kernel/signal.c +85 -30

187 lines not on this page · raw message

#4 lore raw

Re: [patch V3 1/8] signal: Prevent exec() race

In reply to Thomas Gleixner

kernel/signal.c static int __send_signal_locked(int sig, · line 1077

Quoted 10 lines
kernel/signal.c +4 -0
@@ -1030,6 +1069,10 @@ static int __send_signal_locked(int sig,
 	lockdep_assert_held(&t->sighand->siglock);
 
 	result = TRACE_SIGNAL_IGNORED;
+
+	if (!task_can_queue_signal(t, type))
+		goto ret;
+
 	if (!prepare_signal(sig, t, force))
 		goto ret;

Should this check sit after prepare_signal() rather than before it?

The comment on task_can_queue_signal() and the changelog both describe the new check as preventing a sigqueue from being queued and leaked, but placing it ahead of prepare_signal() also skips the process wide stop/continue handling, which prepare_signal() documents as happening even for signals that are never queued. Moving the check below prepare_signal() looks like it would keep the stop and continue side effects while still preventing the sigqueue from being queued, since prepare_signal() only removes entries from task::pending and never adds any.

Andrea

author maintainer
#5 lore raw

PATCH v3 2/8 exec: Cleanup POSIX timers right after de_thread()

In reply to Thomas Gleixner

From: Hyunwoo Kim <[email protected]>

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

Move the POSIX timer cleanup right after de_thread() before any of the later failure conditions brings the task into do_exit().

[ tglx: Move the cleanup right after de_thread() ]

---
Changes in v3:
Move the cleanup right after de_thread() - Oleg
Rework change log

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 |   29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)
fs/exec.c +21 -8
@@ -1115,6 +1115,17 @@ static struct file *bprm_identity_file(c
 	return bprm->file;
 }
 
+static void posixtimer_exec(struct task_struct *me)
+{
+#ifdef CONFIG_POSIX_TIMERS
+	spin_lock_irq(&me->sighand->siglock);
+	posix_cpu_timers_exit(me);
+	spin_unlock_irq(&me->sighand->siglock);
+	exit_itimers(me);
+	flush_itimer_signals();
+#endif
+}
+
 /*
  * Calling this is the point of no return. None of the failures will be
  * seen by userspace since either the process is already taking a fatal
@@ -1152,6 +1163,16 @@ int begin_new_exec(struct linux_binprm *
 	retval = de_thread(me);
 	if (retval)
 		goto out;
+
+	/*
+	 * This must be done here to ensure that POSIX CPU timers which were
+	 * armed on the current task are dequeued from me::posix_cputimers.
+	 * Otherwise in case of a TID switch the deletion of the related POSIX
+	 * timer would not remove an enqueued timer because the TID lookup
+	 * of the old TID fails.
+	 */
+	posixtimer_exec(me);
+
 	/* see the comment in check_unsafe_exec() */
 	current->fs->in_exec = 0;
 	/*
@@ -1192,14 +1213,6 @@ int begin_new_exec(struct linux_binprm *
 	if (retval)
 		goto out_unlock;
 
-#ifdef CONFIG_POSIX_TIMERS
-	spin_lock_irq(&me->sighand->siglock);
-	posix_cpu_timers_exit(me);
-	spin_unlock_irq(&me->sighand->siglock);
-	exit_itimers(me);
-	flush_itimer_signals();
-#endif
-
 	/*
 	 * Make the signal table private.
 	 */
merge bot Applied
#6 lore raw

[tip: timers/core] exec: Cleanup POSIX timers right after de_thread()

The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     acb03d3881818581052924a9bbbe92b8741ed448
Gitweb:        https://git.kernel.org/tip/acb03d3881818581052924a9bbbe92b8741ed448
Author:        Hyunwoo Kim <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:17 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:03 +02:00

exec: Cleanup POSIX timers right after de_thread()

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

Move the POSIX timer cleanup right after de_thread() before any of the later failure conditions brings the task into do_exit().

[ tglx: Move the cleanup right after de_thread() ]

---
 fs/exec.c | 29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)
fs/exec.c +21 -8
index d3081c8..8196434 100644
@@ -1115,6 +1115,17 @@ static struct file *bprm_identity_file(const struct linux_binprm *bprm)
 	return bprm->file;
 }
 
+static void posixtimer_exec(struct task_struct *me)
+{
+#ifdef CONFIG_POSIX_TIMERS
+	spin_lock_irq(&me->sighand->siglock);
+	posix_cpu_timers_exit(me);
+	spin_unlock_irq(&me->sighand->siglock);
+	exit_itimers(me);
+	flush_itimer_signals();
+#endif
+}
+
 /*
  * Calling this is the point of no return. None of the failures will be
  * seen by userspace since either the process is already taking a fatal
@@ -1152,6 +1163,16 @@ int begin_new_exec(struct linux_binprm * bprm)
 	retval = de_thread(me);
 	if (retval)
 		goto out;
+
+	/*
+	 * This must be done here to ensure that POSIX CPU timers which were
+	 * armed on the current task are dequeued from me::posix_cputimers.
+	 * Otherwise in case of a TID switch the deletion of the related POSIX
+	 * timer would not remove an enqueued timer because the TID lookup
+	 * of the old TID fails.
+	 */
+	posixtimer_exec(me);
+
 	/* see the comment in check_unsafe_exec() */
 	current->fs->in_exec = 0;
 	/*
@@ -1206,14 +1227,6 @@ int begin_new_exec(struct linux_binprm * bprm)
 	if (retval)
 		goto out_unlock;
 
-#ifdef CONFIG_POSIX_TIMERS
-	spin_lock_irq(&me->sighand->siglock);
-	posix_cpu_timers_exit(me);
-	spin_unlock_irq(&me->sighand->siglock);
-	exit_itimers(me);
-	flush_itimer_signals();
-#endif
-
 	/*
 	 * Make the signal table private.
 	 */
author maintainer
#7 lore raw

PATCH v3 3/8 posix-timers: Move posixtimer_exec_cleanup() out of exec.c

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Move it to the POSIX timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

---
 fs/exec.c                    |   13 +------------
 include/linux/posix-timers.h |    3 +++
 kernel/time/posix-timers.c   |    9 +++++++++
 3 files changed, 13 insertions(+), 12 deletions(-)
fs/exec.c +1 -12
@@ -1115,17 +1115,6 @@ static struct file *bprm_identity_file(c
 	return bprm->file;
 }
 
-static void posixtimer_exec(struct task_struct *me)
-{
-#ifdef CONFIG_POSIX_TIMERS
-	spin_lock_irq(&me->sighand->siglock);
-	posix_cpu_timers_exit(me);
-	spin_unlock_irq(&me->sighand->siglock);
-	exit_itimers(me);
-	flush_itimer_signals();
-#endif
-}
-
 /*
  * Calling this is the point of no return. None of the failures will be
  * seen by userspace since either the process is already taking a fatal
@@ -1171,7 +1160,7 @@ int begin_new_exec(struct linux_binprm *
 	 * timer would not remove an enqueued timer because the TID lookup
 	 * of the old TID fails.
 	 */
-	posixtimer_exec(me);
+	posixtimer_exec();
 
 	/* see the comment in check_unsafe_exec() */
 	current->fs->in_exec = 0;
include/linux/posix-timers.h +3 -0
@@ -232,6 +232,8 @@ void set_process_cpu_timer(struct task_s
 int update_rlimit_cpu(struct task_struct *task, unsigned long rlim_new);
 
 #ifdef CONFIG_POSIX_TIMERS
+void posixtimer_exec(void);
+
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
 	if (rcuref_put(&tmr->rcuref))
@@ -259,6 +261,7 @@ static inline bool posixtimer_valid(cons
 	return !(val & 0x1UL);
 }
 #else  /* CONFIG_POSIX_TIMERS */
+static inline void posixtimer_exec(void) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
kernel/time/posix-timers.c +9 -0
@@ -1120,6 +1120,15 @@ void exit_itimers(struct task_struct *ts
 	}
 }
 
+void posixtimer_exec(void)
+{
+	scoped_guard(spinlock_irq, &current->sighand->siglock)
+		posix_cpu_timers_exit(current);
+
+	exit_itimers(current);
+	flush_itimer_signals();
+}
+
 SYSCALL_DEFINE2(clock_settime, const clockid_t, which_clock,
 		const struct __kernel_timespec __user *, tp)
 {
Reviewed-by on v3 3/8
#8 lore raw

Re: [patch V3 3/8] posix-timers: Move posixtimer_exec_cleanup() out of exec.c

Quoted 7 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Move it to the POSIX timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

#9 lore raw

[tip: timers/core] posix-timers: Move posixtimer_exec_cleanup() out of exec.c

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     7117334f022ba0cc7475f688efa68dd56e60f65b
Gitweb:        https://git.kernel.org/tip/7117334f022ba0cc7475f688efa68dd56e60f65b
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:21 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:03 +02:00

posix-timers: Move posixtimer_exec_cleanup() out of exec.c

Move it to the POSIX timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

---
 fs/exec.c                    | 13 +------------
 include/linux/posix-timers.h |  3 +++
 kernel/time/posix-timers.c   |  9 +++++++++
 3 files changed, 13 insertions(+), 12 deletions(-)
fs/exec.c +1 -12
index 8196434..1fc5671 100644
@@ -1115,17 +1115,6 @@ static struct file *bprm_identity_file(const struct linux_binprm *bprm)
 	return bprm->file;
 }
 
-static void posixtimer_exec(struct task_struct *me)
-{
-#ifdef CONFIG_POSIX_TIMERS
-	spin_lock_irq(&me->sighand->siglock);
-	posix_cpu_timers_exit(me);
-	spin_unlock_irq(&me->sighand->siglock);
-	exit_itimers(me);
-	flush_itimer_signals();
-#endif
-}
-
 /*
  * Calling this is the point of no return. None of the failures will be
  * seen by userspace since either the process is already taking a fatal
@@ -1171,7 +1160,7 @@ int begin_new_exec(struct linux_binprm * bprm)
 	 * timer would not remove an enqueued timer because the TID lookup
 	 * of the old TID fails.
 	 */
-	posixtimer_exec(me);
+	posixtimer_exec();
 
 	/* see the comment in check_unsafe_exec() */
 	current->fs->in_exec = 0;
include/linux/posix-timers.h +3 -0
index 9a1a0c6..7b31da6 100644
@@ -232,6 +232,8 @@ void set_process_cpu_timer(struct task_struct *task, unsigned int clock_idx,
 int update_rlimit_cpu(struct task_struct *task, unsigned long rlim_new);
 
 #ifdef CONFIG_POSIX_TIMERS
+void posixtimer_exec(void);
+
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
 	if (rcuref_put(&tmr->rcuref))
@@ -259,6 +261,7 @@ static inline bool posixtimer_valid(const struct k_itimer *timer)
 	return !(val & 0x1UL);
 }
 #else  /* CONFIG_POSIX_TIMERS */
+static inline void posixtimer_exec(void) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
kernel/time/posix-timers.c +9 -0
index 436ba79..9611ed4 100644
@@ -1120,6 +1120,15 @@ void exit_itimers(struct task_struct *tsk)
 	}
 }
 
+void posixtimer_exec(void)
+{
+	scoped_guard(spinlock_irq, &current->sighand->siglock)
+		posix_cpu_timers_exit(current);
+
+	exit_itimers(current);
+	flush_itimer_signals();
+}
+
 SYSCALL_DEFINE2(clock_settime, const clockid_t, which_clock,
 		const struct __kernel_timespec __user *, tp)
 {
#10 lore raw

Re: [patch V3 3/8] posix-timers: Move posixtimer_exec_cleanup() out of exec.c

In reply to Thomas Gleixner
Quoted 9 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Move it to the POSIX timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

Maybe I'm being pedantic, but the subject line names posixtimer_exec_cleanup(), while the helper that actually moves is posixtimer_exec().

Andrea

author maintainer
#11 lore raw

PATCH v3 4/8 posix-timers: Move POSIX timer group exit related code out of do_exit()

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Move the POSIX timer group exit handling into the posix timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

---
 include/linux/posix-timers.h |    2 ++
 include/linux/sched/task.h   |    1 -
 kernel/exit.c                |    7 +++----
 kernel/time/posix-timers.c   |   16 +++++++++-------
 4 files changed, 14 insertions(+), 12 deletions(-)
include/linux/posix-timers.h +2 -0
@@ -233,6 +233,7 @@ int update_rlimit_cpu(struct task_struct
 
 #ifdef CONFIG_POSIX_TIMERS
 void posixtimer_exec(void);
+void posixtimer_exit(void);
 
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
@@ -262,6 +263,7 @@ static inline bool posixtimer_valid(cons
 }
 #else  /* CONFIG_POSIX_TIMERS */
 static inline void posixtimer_exec(void) { }
+static inline void posixtimer_exit(void) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
include/linux/sched/task.h +0 -1
@@ -94,7 +94,6 @@ static inline void exit_thread(struct ta
 extern __noreturn void do_group_exit(int);
 
 extern void exit_files(struct task_struct *);
-extern void exit_itimers(struct task_struct *);
 
 extern pid_t kernel_clone(struct kernel_clone_args *kargs);
 struct task_struct *copy_process(struct pid *pid, int trace, int node,
kernel/exit.c +3 -4
@@ -963,13 +963,12 @@ void __noreturn do_exit(long code)
 			panic("Attempted to kill init! exitcode=0x%08x\n",
 				tsk->signal->group_exit_code ?: (int)code);
 
-#ifdef CONFIG_POSIX_TIMERS
-		hrtimer_cancel(&tsk->signal->real_timer);
-		exit_itimers(tsk);
-#endif
+		posixtimer_exit();
+
 		if (tsk->mm)
 			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
 	}
+
 	acct_collect(code, group_dead);
 	if (group_dead)
 		tty_audit_exit();
kernel/time/posix-timers.c +9 -7
@@ -1077,13 +1077,9 @@ SYSCALL_DEFINE1(timer_delete, timer_t, t
 	return 0;
 }
 
-/*
- * Invoked from do_exit() when the last thread of a thread group exits.
- * At that point no other task can access the timers of the dying
- * task anymore.
- */
-void exit_itimers(struct task_struct *tsk)
+static void posixtimer_delete_timers(void)
 {
+	struct task_struct *tsk = current;
 	struct hlist_head timers;
 	struct hlist_node *next;
 	struct k_itimer *timer;
@@ -1120,12 +1116,18 @@ void exit_itimers(struct task_struct *ts
 	}
 }
 
+void posixtimer_exit(void)
+{
+	hrtimer_cancel(&current->signal->real_timer);
+	posixtimer_delete_timers();
+}
+
 void posixtimer_exec(void)
 {
 	scoped_guard(spinlock_irq, &current->sighand->siglock)
 		posix_cpu_timers_exit(current);
 
-	exit_itimers(current);
+	posixtimer_delete_timers();
 	flush_itimer_signals();
 }
 
Reviewed-by on v3 4/8
#12 lore raw

Re: [patch V3 4/8] posix-timers: Move POSIX timer group exit related code out of do_exit()

Quoted 7 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Move the POSIX timer group exit handling into the posix timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

#13 lore raw

[tip: timers/core] posix-timers: Move POSIX timer group exit related code out of do_exit()

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     24adce0b86c940ac3dcee0bcd75e799634fc9664
Gitweb:        https://git.kernel.org/tip/24adce0b86c940ac3dcee0bcd75e799634fc9664
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:26 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:03 +02:00

posix-timers: Move POSIX timer group exit related code out of do_exit()

Move the POSIX timer group exit handling into the posix timer code and provide a proper stub when POSIX timers are disabled in Kconfig.

No functional change.

---
 include/linux/posix-timers.h |  2 ++
 include/linux/sched/task.h   |  1 -
 kernel/exit.c                |  7 +++----
 kernel/time/posix-timers.c   | 16 +++++++++-------
 4 files changed, 14 insertions(+), 12 deletions(-)
include/linux/posix-timers.h +2 -0
index 7b31da6..3050f9e 100644
@@ -233,6 +233,7 @@ int update_rlimit_cpu(struct task_struct *task, unsigned long rlim_new);
 
 #ifdef CONFIG_POSIX_TIMERS
 void posixtimer_exec(void);
+void posixtimer_exit(void);
 
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
@@ -262,6 +263,7 @@ static inline bool posixtimer_valid(const struct k_itimer *timer)
 }
 #else  /* CONFIG_POSIX_TIMERS */
 static inline void posixtimer_exec(void) { }
+static inline void posixtimer_exit(void) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
include/linux/sched/task.h +0 -1
index e0c1ca8..90ed5bc 100644
@@ -94,7 +94,6 @@ static inline void exit_thread(struct task_struct *tsk)
 extern __noreturn void do_group_exit(int);
 
 extern void exit_files(struct task_struct *);
-extern void exit_itimers(struct task_struct *);
 
 extern pid_t kernel_clone(struct kernel_clone_args *kargs);
 struct task_struct *copy_process(struct pid *pid, int trace, int node,
kernel/exit.c +3 -4
index 424c44a..4ca9759 100644
@@ -966,13 +966,12 @@ void __noreturn do_exit(long code)
 			panic("Attempted to kill init! exitcode=0x%08x\n",
 				tsk->signal->group_exit_code ?: (int)code);
 
-#ifdef CONFIG_POSIX_TIMERS
-		hrtimer_cancel(&tsk->signal->real_timer);
-		exit_itimers(tsk);
-#endif
+		posixtimer_exit();
+
 		if (tsk->mm)
 			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
 	}
+
 	acct_collect(code, group_dead);
 	if (group_dead)
 		tty_audit_exit();
kernel/time/posix-timers.c +9 -7
index 9611ed4..8fb040a 100644
@@ -1077,13 +1077,9 @@ SYSCALL_DEFINE1(timer_delete, timer_t, timer_id)
 	return 0;
 }
 
-/*
- * Invoked from do_exit() when the last thread of a thread group exits.
- * At that point no other task can access the timers of the dying
- * task anymore.
- */
-void exit_itimers(struct task_struct *tsk)
+static void posixtimer_delete_timers(void)
 {
+	struct task_struct *tsk = current;
 	struct hlist_head timers;
 	struct hlist_node *next;
 	struct k_itimer *timer;
@@ -1120,12 +1116,18 @@ void exit_itimers(struct task_struct *tsk)
 	}
 }
 
+void posixtimer_exit(void)
+{
+	hrtimer_cancel(&current->signal->real_timer);
+	posixtimer_delete_timers();
+}
+
 void posixtimer_exec(void)
 {
 	scoped_guard(spinlock_irq, &current->sighand->siglock)
 		posix_cpu_timers_exit(current);
 
-	exit_itimers(current);
+	posixtimer_delete_timers();
 	flush_itimer_signals();
 }
 
author maintainer
#14 lore raw

PATCH v3 5/8 posix-cpu-timers: Move inlines out of public header

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

They are only used in the POSIX CPU timer code. No point in exposing them globally and parsing them for nothing.

---
 include/linux/posix-timers.h   |   32 --------------------------------
 kernel/time/posix-cpu-timers.c |   32 ++++++++++++++++++++++++++++++++
 2 files changed, 32 insertions(+), 32 deletions(-)
include/linux/posix-timers.h +0 -32
@@ -66,38 +66,6 @@ struct cpu_timer {
 	struct task_struct __rcu	*handling;
 };
 
-static inline bool cpu_timer_enqueue(struct timerqueue_head *head,
-				     struct cpu_timer *ctmr)
-{
-	ctmr->head = head;
-	return timerqueue_add(head, &ctmr->node);
-}
-
-static inline bool cpu_timer_queued(struct cpu_timer *ctmr)
-{
-	return !!ctmr->head;
-}
-
-static inline bool cpu_timer_dequeue(struct cpu_timer *ctmr)
-{
-	if (cpu_timer_queued(ctmr)) {
-		timerqueue_del(ctmr->head, &ctmr->node);
-		ctmr->head = NULL;
-		return true;
-	}
-	return false;
-}
-
-static inline u64 cpu_timer_getexpires(struct cpu_timer *ctmr)
-{
-	return ctmr->node.expires;
-}
-
-static inline void cpu_timer_setexpires(struct cpu_timer *ctmr, u64 exp)
-{
-	ctmr->node.expires = exp;
-}
-
 static inline void posix_cputimers_init(struct posix_cputimers *pct)
 {
 	memset(pct, 0, sizeof(*pct));
kernel/time/posix-cpu-timers.c +32 -0
@@ -438,6 +438,38 @@ static void trigger_base_recalc_expires(
 	base->nextevt = 0;
 }
 
+static inline bool cpu_timer_enqueue(struct timerqueue_head *head,
+				     struct cpu_timer *ctmr)
+{
+	ctmr->head = head;
+	return timerqueue_add(head, &ctmr->node);
+}
+
+static inline bool cpu_timer_queued(struct cpu_timer *ctmr)
+{
+	return !!ctmr->head;
+}
+
+static inline bool cpu_timer_dequeue(struct cpu_timer *ctmr)
+{
+	if (cpu_timer_queued(ctmr)) {
+		timerqueue_del(ctmr->head, &ctmr->node);
+		ctmr->head = NULL;
+		return true;
+	}
+	return false;
+}
+
+static inline u64 cpu_timer_getexpires(struct cpu_timer *ctmr)
+{
+	return ctmr->node.expires;
+}
+
+static inline void cpu_timer_setexpires(struct cpu_timer *ctmr, u64 exp)
+{
+	ctmr->node.expires = exp;
+}
+
 /*
  * Dequeue the timer and reset the base if it was its earliest expiration.
  * It makes sure the next tick recalculates the base next expiration so we
Reviewed-by on v3 5/8
#15 lore raw

Re: [patch V3 5/8] posix-cpu-timers: Move inlines out of public header

Quoted 5 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

They are only used in the POSIX CPU timer code. No point in exposing them globally and parsing them for nothing.

#16 lore raw

[tip: timers/core] posix-cpu-timers: Move inlines out of public header

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     8e9ed3b66f409e324dade4a07fc08dae43160ec6
Gitweb:        https://git.kernel.org/tip/8e9ed3b66f409e324dade4a07fc08dae43160ec6
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:31 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:03 +02:00

posix-cpu-timers: Move inlines out of public header

They are only used in the POSIX CPU timer code. No point in exposing them globally and parsing them for nothing.

---
 include/linux/posix-timers.h   | 32 --------------------------------
 kernel/time/posix-cpu-timers.c | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 32 insertions(+), 32 deletions(-)
include/linux/posix-timers.h +0 -32
index 3050f9e..0033a06 100644
@@ -66,38 +66,6 @@ struct cpu_timer {
 	struct task_struct __rcu	*handling;
 };
 
-static inline bool cpu_timer_enqueue(struct timerqueue_head *head,
-				     struct cpu_timer *ctmr)
-{
-	ctmr->head = head;
-	return timerqueue_add(head, &ctmr->node);
-}
-
-static inline bool cpu_timer_queued(struct cpu_timer *ctmr)
-{
-	return !!ctmr->head;
-}
-
-static inline bool cpu_timer_dequeue(struct cpu_timer *ctmr)
-{
-	if (cpu_timer_queued(ctmr)) {
-		timerqueue_del(ctmr->head, &ctmr->node);
-		ctmr->head = NULL;
-		return true;
-	}
-	return false;
-}
-
-static inline u64 cpu_timer_getexpires(struct cpu_timer *ctmr)
-{
-	return ctmr->node.expires;
-}
-
-static inline void cpu_timer_setexpires(struct cpu_timer *ctmr, u64 exp)
-{
-	ctmr->node.expires = exp;
-}
-
 static inline void posix_cputimers_init(struct posix_cputimers *pct)
 {
 	memset(pct, 0, sizeof(*pct));
kernel/time/posix-cpu-timers.c +32 -0
index d73d31c..e635834 100644
@@ -438,6 +438,38 @@ static void trigger_base_recalc_expires(struct k_itimer *timer,
 	base->nextevt = 0;
 }
 
+static inline bool cpu_timer_enqueue(struct timerqueue_head *head,
+				     struct cpu_timer *ctmr)
+{
+	ctmr->head = head;
+	return timerqueue_add(head, &ctmr->node);
+}
+
+static inline bool cpu_timer_queued(struct cpu_timer *ctmr)
+{
+	return !!ctmr->head;
+}
+
+static inline bool cpu_timer_dequeue(struct cpu_timer *ctmr)
+{
+	if (cpu_timer_queued(ctmr)) {
+		timerqueue_del(ctmr->head, &ctmr->node);
+		ctmr->head = NULL;
+		return true;
+	}
+	return false;
+}
+
+static inline u64 cpu_timer_getexpires(struct cpu_timer *ctmr)
+{
+	return ctmr->node.expires;
+}
+
+static inline void cpu_timer_setexpires(struct cpu_timer *ctmr, u64 exp)
+{
+	ctmr->node.expires = exp;
+}
+
 /*
  * Dequeue the timer and reset the base if it was its earliest expiration.
  * It makes sure the next tick recalculates the base next expiration so we
author maintainer
#17 lore raw

PATCH v3 6/8 posix-cpu-timers: Use PF_EXITING to indicate exit

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Right now POSIX CPU timers use task::exit_state to check whether a task is exiting. That works correctly, but exit_state is set later in do_exit() and too late for allowing to cleanup POSIX timers earlier.

It does not matter in case of exit whether the cutoff is a bit earlier.

---
 kernel/time/posix-cpu-timers.c |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
kernel/time/posix-cpu-timers.c +1 -1
@@ -1505,7 +1505,7 @@ void run_posix_cpu_timers(void)
 	 * posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
 	 * miss timer->it.cpu.firing != 0.
 	 */
-	if (tsk->exit_state)
+	if (tsk->flags & PF_EXITING)
 		return;
 
 	/*
maintainer Reviewed-by on v3 6/8
#18 lore raw

Re: [patch V3 6/8] posix-cpu-timers: Use PF_EXITING to indicate exit

Quoted 9 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Right now POSIX CPU timers use task::exit_state to check whether a task is exiting. That works correctly, but exit_state is set later in do_exit() and too late for allowing to cleanup POSIX timers earlier.

It does not matter in case of exit whether the cutoff is a bit earlier.

Quoted 18 lines
---
 kernel/time/posix-cpu-timers.c |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
kernel/time/posix-cpu-timers.c +1 -1
@@ -1505,7 +1505,7 @@ void run_posix_cpu_timers(void)
 	 * posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
 	 * miss timer->it.cpu.firing != 0.
 	 */
-	if (tsk->exit_state)
+	if (tsk->flags & PF_EXITING)
 		return;
 
 	/*
Signature · 2 lines
Frederic Weisbecker
SUSE Labs
Reviewed-by on v3 6/8
#19 lore raw

Re: [patch V3 6/8] posix-cpu-timers: Use PF_EXITING to indicate exit

In reply to Thomas Gleixner

kernel/time/posix-cpu-timers.c void run_posix_cpu_timers(void) · line 1509

Quoted 10 lines · Thomas Gleixner
kernel/time/posix-cpu-timers.c +1 -1
@@ -1505,7 +1505,7 @@ void run_posix_cpu_timers(void)
 	 * posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
 	 * miss timer->it.cpu.firing != 0.
 	 */
-	if (tsk->exit_state)
+	if (tsk->flags & PF_EXITING)
 		return;
#20 lore raw

[tip: timers/core] posix-cpu-timers: Use PF_EXITING to indicate exit

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     760ad335d600b1d45f05bc5fad59c05d909710be
Gitweb:        https://git.kernel.org/tip/760ad335d600b1d45f05bc5fad59c05d909710be
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:35 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:04 +02:00

posix-cpu-timers: Use PF_EXITING to indicate exit

Right now POSIX CPU timers use task::exit_state to check whether a task is exiting. That works correctly, but exit_state is set later in do_exit() and too late for allowing to cleanup POSIX timers earlier.

It does not matter in case of exit whether the cutoff is a bit earlier.

---
 kernel/time/posix-cpu-timers.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
kernel/time/posix-cpu-timers.c +1 -1
index e635834..cf4bd1f 100644
@@ -1505,7 +1505,7 @@ void run_posix_cpu_timers(void)
 	 * posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
 	 * miss timer->it.cpu.firing != 0.
 	 */
-	if (tsk->exit_state)
+	if (tsk->flags & PF_EXITING)
 		return;
 
 	/*
#21 lore raw

Re: [patch V3 6/8] posix-cpu-timers: Use PF_EXITING to indicate exit

In reply to Thomas Gleixner

kernel/time/posix-cpu-timers.c void run_posix_cpu_timers(void) · line 1509

Quoted 7 lines
kernel/time/posix-cpu-timers.c +1 -1
@@ -1505,7 +1505,7 @@ void run_posix_cpu_timers(void)
 	 * posix_cpu_timer_del() may fail to lock_task_sighand(tsk) and
 	 * miss timer->it.cpu.firing != 0.
 	 */
-	if (tsk->exit_state)
+	if (tsk->flags & PF_EXITING)
 		return;

An inline comment in handle_posix_cpu_timers() seems to be the only remaining reference to the exit_state check in the tree. The same comment is still in place at the end of the series, so nothing later seems to update it. Should that comment be updated to refer to the PF_EXITING check instead?

Andrea

author maintainer
#22 lore raw

PATCH v3 7/8 posix-cpu-timers: Prevent enqueueing when PF_EXITING is set

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

To prepare for cleaning up POSIX CPU timers in do_exit(), prevent enqueueing POSIX CPU timers on a task which has PF_EXITING set. Queueing a timer on such a task is pointless because the task won't expire the timer anymore.

The same applies to process wide timers when tsk::signal::flags has SIGNAL_GROUP_EXIT set.

Pretending that the timer is armed allows to keep the POSIX timer mechanism "working" so that the timer stays accessible up to the point where a task is unhashed.

---
 kernel/time/posix-cpu-timers.c |   28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
kernel/time/posix-cpu-timers.c +28 -0
@@ -628,6 +628,7 @@ static int posix_cpu_timer_del(struct k_
 	}
 
 	if (!ret) {
+		WARN_ON_ONCE(cpu_timer_queued(&timer->it.cpu));
 		put_pid(timer->it.cpu.pid);
 		timer->it_status = POSIX_TIMER_DISARMED;
 	}
@@ -675,6 +676,27 @@ void posix_cpu_timers_exit_group(struct
 }
 
 /*
+ * This function validates that POSIX CPU timers can be safely enqueued on the
+ * target task.
+ *
+ * Enqueue is allowed when PF_EXITING is not set. If set then it is only allowed
+ * for process shared timers (type = PIDTYPE_TGID) as long as tsk::signal::flags
+ * does not have SIGNAL_GROUP_EXIT set. PIDTYPE_PID targets are not allowed at
+ * all when the task has PF_EXITING set.
+ *
+ * This guarantees that after the POSIX timer cleanup in posixtimer_exit() no
+ * POSIX CPU timers are queued on the task or in case of a group exit on the
+ * process.
+ */
+static inline bool task_can_enqueue_timer(struct task_struct *tsk, enum pid_type type)
+{
+	if (likely(!(tsk->flags & PF_EXITING)))
+		return true;
+
+	return type == PIDTYPE_TGID && !(tsk->signal->flags & SIGNAL_GROUP_EXIT);
+}
+
+/*
  * Insert the timer on the appropriate list before any timers that
  * expire later.  This must be called with the sighand lock held.
  */
@@ -684,7 +706,13 @@ static void arm_timer(struct k_itimer *t
 	struct cpu_timer *ctmr = &timer->it.cpu;
 	u64 newexp = cpu_timer_getexpires(ctmr);
 
+	lockdep_assert_held(&p->sighand->siglock);
+
 	timer->it_status = POSIX_TIMER_ARMED;
+
+	if (unlikely(!task_can_enqueue_timer(p, clock_pid_type(timer->it_clock))))
+		return;
+
 	if (!cpu_timer_enqueue(&base->tqhead, ctmr))
 		return;
 
maintainer Reviewed-by on v3 7/8
#23 lore raw

Re: [patch V3 7/8] posix-cpu-timers: Prevent enqueueing when PF_EXITING is set

Quoted 10 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

To prepare for cleaning up POSIX CPU timers in do_exit(), prevent enqueueing POSIX CPU timers on a task which has PF_EXITING set. Queueing a timer on such a task is pointless because the task won't expire the timer anymore.

The same applies to process wide timers when tsk::signal::flags has SIGNAL_GROUP_EXIT set.

Quoted 6 lines

Pretending that the timer is armed allows to keep the POSIX timer mechanism "working" so that the timer stays accessible up to the point where a task is unhashed.

Signature · 2 lines
Frederic Weisbecker
SUSE Labs
#24 lore raw

[tip: timers/core] posix-cpu-timers: Prevent enqueueing when PF_EXITING is set

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     54ad1e0ea42c3dc25de39b09220af408e4913820
Gitweb:        https://git.kernel.org/tip/54ad1e0ea42c3dc25de39b09220af408e4913820
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:40 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:04 +02:00

posix-cpu-timers: Prevent enqueueing when PF_EXITING is set

To prepare for cleaning up POSIX CPU timers in do_exit(), prevent enqueueing POSIX CPU timers on a task which has PF_EXITING set. Queueing a timer on such a task is pointless because the task won't expire the timer anymore.

The same applies to process wide timers when tsk::signal::flags has SIGNAL_GROUP_EXIT set.

Pretending that the timer is armed allows to keep the POSIX timer mechanism "working" so that the timer stays accessible up to the point where a task is unhashed.

---
 kernel/time/posix-cpu-timers.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
kernel/time/posix-cpu-timers.c +28 -0
index cf4bd1f..ddaf81b 100644
@@ -628,6 +628,7 @@ static int posix_cpu_timer_del(struct k_itimer *timer)
 	}
 
 	if (!ret) {
+		WARN_ON_ONCE(cpu_timer_queued(&timer->it.cpu));
 		put_pid(timer->it.cpu.pid);
 		timer->it_status = POSIX_TIMER_DISARMED;
 	}
@@ -675,6 +676,27 @@ void posix_cpu_timers_exit_group(struct task_struct *tsk)
 }
 
 /*
+ * This function validates that POSIX CPU timers can be safely enqueued on the
+ * target task.
+ *
+ * Enqueue is allowed when PF_EXITING is not set. If set then it is only allowed
+ * for process shared timers (type = PIDTYPE_TGID) as long as tsk::signal::flags
+ * does not have SIGNAL_GROUP_EXIT set. PIDTYPE_PID targets are not allowed at
+ * all when the task has PF_EXITING set.
+ *
+ * This guarantees that after the POSIX timer cleanup in posixtimer_exit() no
+ * POSIX CPU timers are queued on the task or in case of a group exit on the
+ * process.
+ */
+static inline bool task_can_enqueue_timer(struct task_struct *tsk, enum pid_type type)
+{
+	if (likely(!(tsk->flags & PF_EXITING)))
+		return true;
+
+	return type == PIDTYPE_TGID && !(tsk->signal->flags & SIGNAL_GROUP_EXIT);
+}
+
+/*
  * Insert the timer on the appropriate list before any timers that
  * expire later.  This must be called with the sighand lock held.
  */
@@ -684,7 +706,13 @@ static void arm_timer(struct k_itimer *timer, struct task_struct *p)
 	struct cpu_timer *ctmr = &timer->it.cpu;
 	u64 newexp = cpu_timer_getexpires(ctmr);
 
+	lockdep_assert_held(&p->sighand->siglock);
+
 	timer->it_status = POSIX_TIMER_ARMED;
+
+	if (unlikely(!task_can_enqueue_timer(p, clock_pid_type(timer->it_clock))))
+		return;
+
 	if (!cpu_timer_enqueue(&base->tqhead, ctmr))
 		return;
 
author maintainer
#25 lore raw

PATCH v3 8/8 posix-timers: Handle exit in do_exit() completely

In reply to Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Now that POSIX CPU timers cannot be enqueued on a task after PF_EXITING is set and process wide timers cannot be enqueued when PF_EXITING is set and the last thread in the group is exiting, it is possible to mop up POSIX timers in do_exit() completely.

This requires to cancel an eventually pending POSIX CPU timer task work right there because do_exit() invokes exit_task_work() later, which would be acting on torn down data.

---
V3: Don't touch pending posix timer work on exec() - Oleg

---
 include/linux/posix-timers.h   |    6 ++----
 kernel/exit.c                  |   10 ++--------
 kernel/time/posix-cpu-timers.c |   41 ++++++++++++++++++++++++++++++++++-------
 kernel/time/posix-timers.c     |   15 +++++++++------
 kernel/time/posix-timers.h     |    3 +++
 5 files changed, 50 insertions(+), 25 deletions(-)
include/linux/posix-timers.h +2 -4
@@ -192,8 +192,6 @@ struct k_itimer {
 } ____cacheline_aligned_in_smp;
 
 void run_posix_cpu_timers(void);
-void posix_cpu_timers_exit(struct task_struct *task);
-void posix_cpu_timers_exit_group(struct task_struct *task);
 void set_process_cpu_timer(struct task_struct *task, unsigned int clock_idx,
 			   u64 *newval, u64 *oldval);
 
@@ -201,7 +199,7 @@ int update_rlimit_cpu(struct task_struct
 
 #ifdef CONFIG_POSIX_TIMERS
 void posixtimer_exec(void);
-void posixtimer_exit(void);
+void posixtimer_exit(bool group_dead);
 
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
@@ -231,7 +229,7 @@ static inline bool posixtimer_valid(cons
 }
 #else  /* CONFIG_POSIX_TIMERS */
 static inline void posixtimer_exec(void) { }
-static inline void posixtimer_exit(void) { }
+static inline void posixtimer_exit(bool group_dead) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
kernel/exit.c +2 -8
@@ -167,12 +167,6 @@ static void __exit_signal(struct release
 					lockdep_tasklist_lock_is_held());
 	spin_lock(&sighand->siglock);
 
-#ifdef CONFIG_POSIX_TIMERS
-	posix_cpu_timers_exit(tsk);
-	if (group_dead)
-		posix_cpu_timers_exit_group(tsk);
-#endif
-
 	if (group_dead) {
 		tty = sig->tty;
 		sig->tty = NULL;
@@ -963,12 +957,12 @@ void __noreturn do_exit(long code)
 			panic("Attempted to kill init! exitcode=0x%08x\n",
 				tsk->signal->group_exit_code ?: (int)code);
 
-		posixtimer_exit();
-
 		if (tsk->mm)
 			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
 	}
 
+	posixtimer_exit(group_dead);
+
 	acct_collect(code, group_dead);
 	if (group_dead)
 		tty_audit_exit();
kernel/time/posix-cpu-timers.c +34 -7

64 lines not on this page · raw message

kernel/time/posix-timers.c +9 -6
@@ -1116,17 +1116,20 @@ static void posixtimer_delete_timers(voi
 	}
 }
 
-void posixtimer_exit(void)
+void posixtimer_exit(bool group_dead)
 {
-	hrtimer_cancel(&current->signal->real_timer);
-	posixtimer_delete_timers();
+	if (group_dead) {
+		hrtimer_cancel(&current->signal->real_timer);
+		posix_cpu_timers_exit_group();
+		posixtimer_delete_timers();
+	} else {
+		posix_cpu_timers_exit_task();
+	}
 }
 
 void posixtimer_exec(void)
 {
-	scoped_guard(spinlock_irq, &current->sighand->siglock)
-		posix_cpu_timers_exit(current);
-
+	posix_cpu_timers_exit_task();
 	posixtimer_delete_timers();
 	flush_itimer_signals();
 }
kernel/time/posix-timers.h +3 -0
@@ -51,3 +51,6 @@ int common_timer_set(struct k_itimer *ti
 		     struct itimerspec64 *old_setting);
 void posix_timer_set_common(struct k_itimer *timer, struct itimerspec64 *new_setting);
 int common_timer_del(struct k_itimer *timer);
+
+void posix_cpu_timers_exit_task(void);
+void posix_cpu_timers_exit_group(void);
Reviewed-by on v3 8/8
#26 lore raw

Re: [patch V3 8/8] posix-timers: Handle exit in do_exit() completely

Quoted 8 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Now that POSIX CPU timers cannot be enqueued on a task after PF_EXITING is set and process wide timers cannot be enqueued when PF_EXITING is set and the last thread in the group is exiting, it is possible to mop up POSIX timers in do_exit() completely.

Quoted 7 lines

This requires to cancel an eventually pending POSIX CPU timer task work right there because do_exit() invokes exit_task_work() later, which would be acting on torn down data.

---
V3: Don't touch pending posix timer work on exec() - Oleg

maintainer Reviewed-by on v3 8/8
#27 lore raw

Re: [patch V3 8/8] posix-timers: Handle exit in do_exit() completely

In reply to Thomas Gleixner
Quoted 7 lines · Thomas Gleixner

From: Thomas Gleixner <[email protected]>

Now that POSIX CPU timers cannot be enqueued on a task after PF_EXITING is set and process wide timers cannot be enqueued when PF_EXITING is set and the last thread in the group is exiting, it is possible to mop up POSIX timers in do_exit() completely.

Quoted 5 lines

This requires to cancel an eventually pending POSIX CPU timer task work right there because do_exit() invokes exit_task_work() later, which would be acting on torn down data.

Signature · 2 lines
Frederic Weisbecker
SUSE Labs
#28 lore raw

[tip: timers/core] posix-timers: Handle exit in do_exit() completely

In reply to Thomas Gleixner
The following commit has been merged into the timers/core branch of tip:

The following commit has been merged into the timers/core branch of tip:

Commit-ID:     7b07d15ed1d78cb49309754a2d14dacdddb058c7
Gitweb:        https://git.kernel.org/tip/7b07d15ed1d78cb49309754a2d14dacdddb058c7
Author:        Thomas Gleixner <[email protected]>
AuthorDate:    Fri, 11 Sep 2026 11:09:44 +02:00
Committer:     Thomas Gleixner <[email protected]>
CommitterDate: Wed, 16 Sep 2026 18:44:04 +02:00

posix-timers: Handle exit in do_exit() completely

Now that POSIX CPU timers cannot be enqueued on a task after PF_EXITING is set and process wide timers cannot be enqueued when PF_EXITING is set and the last thread in the group is exiting, it is possible to mop up POSIX timers in do_exit() completely.

This requires to cancel an eventually pending POSIX CPU timer task work right there because do_exit() invokes exit_task_work() later, which would be acting on torn down data.

---
 include/linux/posix-timers.h   |  6 +----
 kernel/exit.c                  | 10 +-------
 kernel/time/posix-cpu-timers.c | 41 +++++++++++++++++++++++++++------
 kernel/time/posix-timers.c     | 15 +++++++-----
 kernel/time/posix-timers.h     |  3 ++-
 5 files changed, 50 insertions(+), 25 deletions(-)
include/linux/posix-timers.h +2 -4
index 0033a06..00767ac 100644
@@ -192,8 +192,6 @@ struct k_itimer {
 } ____cacheline_aligned_in_smp;
 
 void run_posix_cpu_timers(void);
-void posix_cpu_timers_exit(struct task_struct *task);
-void posix_cpu_timers_exit_group(struct task_struct *task);
 void set_process_cpu_timer(struct task_struct *task, unsigned int clock_idx,
 			   u64 *newval, u64 *oldval);
 
@@ -201,7 +199,7 @@ int update_rlimit_cpu(struct task_struct *task, unsigned long rlim_new);
 
 #ifdef CONFIG_POSIX_TIMERS
 void posixtimer_exec(void);
-void posixtimer_exit(void);
+void posixtimer_exit(bool group_dead);
 
 static inline void posixtimer_putref(struct k_itimer *tmr)
 {
@@ -231,7 +229,7 @@ static inline bool posixtimer_valid(const struct k_itimer *timer)
 }
 #else  /* CONFIG_POSIX_TIMERS */
 static inline void posixtimer_exec(void) { }
-static inline void posixtimer_exit(void) { }
+static inline void posixtimer_exit(bool group_dead) { }
 static inline void posixtimer_sigqueue_getref(struct sigqueue *q) { }
 static inline void posixtimer_sigqueue_putref(struct sigqueue *q) { }
 #endif /* !CONFIG_POSIX_TIMERS */
kernel/exit.c +2 -8
index 4ca9759..06a1eb1 100644
@@ -167,12 +167,6 @@ static void __exit_signal(struct release_task_post *post, struct task_struct *ts
 					lockdep_tasklist_lock_is_held());
 	spin_lock(&sighand->siglock);
 
-#ifdef CONFIG_POSIX_TIMERS
-	posix_cpu_timers_exit(tsk);
-	if (group_dead)
-		posix_cpu_timers_exit_group(tsk);
-#endif
-
 	if (group_dead) {
 		tty = sig->tty;
 		sig->tty = NULL;
@@ -966,12 +960,12 @@ void __noreturn do_exit(long code)
 			panic("Attempted to kill init! exitcode=0x%08x\n",
 				tsk->signal->group_exit_code ?: (int)code);
 
-		posixtimer_exit();
-
 		if (tsk->mm)
 			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
 	}
 
+	posixtimer_exit(group_dead);
+
 	acct_collect(code, group_dead);
 	if (group_dead)
 		tty_audit_exit();
kernel/time/posix-cpu-timers.c +34 -7

64 lines not on this page · raw message

kernel/time/posix-timers.c +9 -6
index 8fb040a..188dbed 100644
@@ -1116,17 +1116,20 @@ static void posixtimer_delete_timers(void)
 	}
 }
 
-void posixtimer_exit(void)
+void posixtimer_exit(bool group_dead)
 {
-	hrtimer_cancel(&current->signal->real_timer);
-	posixtimer_delete_timers();
+	if (group_dead) {
+		hrtimer_cancel(&current->signal->real_timer);
+		posix_cpu_timers_exit_group();
+		posixtimer_delete_timers();
+	} else {
+		posix_cpu_timers_exit_task();
+	}
 }
 
 void posixtimer_exec(void)
 {
-	scoped_guard(spinlock_irq, &current->sighand->siglock)
-		posix_cpu_timers_exit(current);
-
+	posix_cpu_timers_exit_task();
 	posixtimer_delete_timers();
 	flush_itimer_signals();
 }
kernel/time/posix-timers.h +3 -0
index 4ea9611..79fd7ea 100644
@@ -51,3 +51,6 @@ int common_timer_set(struct k_itimer *timr, int flags,
 		     struct itimerspec64 *old_setting);
 void posix_timer_set_common(struct k_itimer *timer, struct itimerspec64 *new_setting);
 int common_timer_del(struct k_itimer *timer);
+
+void posix_cpu_timers_exit_task(void);
+void posix_cpu_timers_exit_group(void);
#29 lore raw

Re: [patch V3 8/8] posix-timers: Handle exit in do_exit() completely

In reply to Thomas Gleixner

kernel/exit.c static void __exit_signal(struct release · line 175

Quoted 10 lines
kernel/exit.c +0 -6
@@ -167,12 +167,6 @@ static void __exit_signal(struct release
 					lockdep_tasklist_lock_is_held());
 	spin_lock(&sighand->siglock);
 
-#ifdef CONFIG_POSIX_TIMERS
-	posix_cpu_timers_exit(tsk);
-	if (group_dead)
-		posix_cpu_timers_exit_group(tsk);
-#endif
-

Now that __exit_signal() no longer touches the CPU timer queues, does the block comment above timer_lock_sighand() need an update in the same patch? It still credits __exit_signal() with invoking posix_cpu_timers_exit()/posix_cpu_timers_exit_group().

The barrier diagram further down in timer_lock_sighand() describes the same old callchain, and it is what the WARN_ON_ONCE() right below the diagram relies on. Would it help to spell the new chain out so that the WARN_ON_ONCE() keeps a documented ordering argument?

kernel/time/posix-cpu-timers.c static void cleanup_timers(struct posix_ · line 668

Quoted 12 lines
kernel/time/posix-cpu-timers.c +4 -3
@@ -661,18 +661,29 @@ static void cleanup_timers(struct posix_
 	cleanup_timerqueue(&pct->bases[CPUCLOCK_SCHED].tqhead);
 }
 
+static inline void posix_cpu_timers_exit_work(void);
+
 /*
- * These are both called with the siglock held, when the current thread
- * is being reaped.  When the final (leader) thread in the group is reaped,
- * posix_cpu_timers_exit_group will be called after posix_cpu_timers_exit.
+ * Invoked from posixtimer_exit_task() after PF_EXITING was set in tsk::flags or
+ * from posixtimer_exec_cleanup().

Should these be posixtimer_exit() and posixtimer_exec()? Neither posixtimer_exit_task() nor posixtimer_exec_cleanup() exists in the tree. The callers are posixtimer_exit()->posix_cpu_timers_exit_task(), posixtimer_exec()->posix_cpu_timers_exit_task() and posix_cpu_timers_exit_group()->posix_cpu_timers_exit_task().

Quoted 4 lines
kernel/time/posix-cpu-timers.c +1 -1
  */
-void posix_cpu_timers_exit(struct task_struct *tsk)
+void posix_cpu_timers_exit_task(void)
 {

kernel/time/posix-cpu-timers.c static void cleanup_timers(struct posix_ · line 679

Quoted 10 lines
kernel/time/posix-cpu-timers.c +7 -2
-	cleanup_timers(&tsk->posix_cputimers);
+	posix_cpu_timers_exit_work();
+
+	guard(spinlock_irq)(&current->sighand->siglock);
+	cleanup_timers(&current->posix_cputimers);
 }
-void posix_cpu_timers_exit_group(struct task_struct *tsk)
+
+/*
+ * Invoked from posixtimer_exit_group() after PF_EXITING was set in tsk::flags.

Same question here: the only caller is posixtimer_exit(), and posixtimer_exit_group() does not exist.

kernel/time/posix-cpu-timers.c static void cleanup_timers(struct posix_ · line 689

Quoted 11 lines
kernel/time/posix-cpu-timers.c +6 -1
+ */
+void posix_cpu_timers_exit_group(void)
 {
-	cleanup_timers(&tsk->signal->posix_cputimers);
+	posix_cpu_timers_exit_task();
+
+	guard(spinlock_irq)(&current->sighand->siglock);
+	cleanup_timers(&current->signal->posix_cputimers);
 }
 
 /*

Andrea