mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Frederic Weisbecker <frederic@kernel.org>
To: Thomas Gleixner <tglx@kernel.org>
Cc: LKML <linux-kernel@vger.kernel.org>,
	"Cc: Hyunwoo Kim" <imv4bel@gmail.com>,
	Oleg Nesterov <oleg@redhat.com>,
	Christian Brauner <brauner@kernel.org>,
	Peter Zijlstra <peterz@infradead.org>,
	John Stultz <jstultz@google.com>, Ingo Molnar <mingo@kernel.org>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	"Eric W. Biederman" <ebiederm@xmission.com>,
	stable@vger.kernel.org
Subject: Re: [patch V2 1/8] signal: Prevent exec() race
Date: Mon, 7 Sep 2026 14:31:43 +0200	[thread overview]
Message-ID: <ap6urw5aAxrKO3Ck@localhost.localdomain> (raw)
In-Reply-To: <20260905185839.667208455@kernel.org>

Le Sat, Sep 05, 2026 at 08:59:01PM +0200, Thomas Gleixner a écrit :
> 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.
> 
> Fixes: fb3bbcfe344e ("exit: change the release_task() paths to call flush_sigqueue() lockless")
> Reported-by: Hyunwoo Kim <imv4bel@gmail.com>
> Debugged-by: Hyunwoo Kim <imv4bel@gmail.com>
> Suggested-by: "Eric W. Biederman" <ebiederm@xmission.com>
> Signed-off-by: Thomas Gleixner <tglx@kernel.org>
> Cc: stable@vger.kernel.org
> Closes: https://patch.msgid.link/aok1rdkBgZsynHZB@v4bel
> ---
> 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 while pending list under the lock and free it lockless
> ---
>  kernel/exit.c   |   11 +++--
>  kernel/signal.c |  103 +++++++++++++++++++++++++++++++++++++++-----------------
>  2 files changed, 78 insertions(+), 36 deletions(-)
> 
> --- a/kernel/exit.c
> +++ b/kernel/exit.c
> @@ -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);
>  
> --- a/kernel/signal.c
> +++ b/kernel/signal.c
> @@ -457,18 +457,28 @@ static void __sigqueue_free(struct sigqu
>  	kmem_cache_free(sigqueue_cachep, q);
>  }
>  
> -void flush_sigqueue(struct sigpending *queue)
> +static void flush_sigqueue_list(struct list_head *head)
>  {
> -	struct sigqueue *q;
> +	struct sigqueue *q, *tmp;
>  
> -	sigemptyset(&queue->signal);
> -	while (!list_empty(&queue->list)) {
> -		q = list_entry(queue->list.next, struct sigqueue , list);
> +	list_for_each_entry_safe(q, tmp, head, list) {
>  		list_del_init(&q->list);
>  		__sigqueue_free(q);
>  	}
>  }
>  
> +void flush_sigqueue(struct sigpending *queue)
> +{
> +	sigemptyset(&queue->signal);
> +	flush_sigqueue_list(&queue->list);
> +}
> +
> +static void sigqueue_dequeue_pending(struct sigpending *queue, struct list_head *head)
> +{
> +	sigemptyset(&queue->signal);
> +	list_splice_init(&queue->list, head);
> +}
> +
>  /*
>   * Flush all pending signals for this kthread.
>   */
> @@ -1019,6 +1029,21 @@ static inline bool legacy_queue(struct s
>  	return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
>  }
>  
> +/*
> + * When PF_EXITING is set the task is on the way out and has t::pending
> + * flushed already. Prevent queueing of PIDTYPE_PID signals as they would
> + * be leaked.
> + */
> +static inline bool task_can_queue_signal(struct task_struct *t, enum pid_type type)
> +{
> +	lockdep_assert_held(&t->sighand->siglock);
> +
> +	if (!(t->flags & PF_EXITING))
> +		return true;
> +
> +	return type != PIDTYPE_PID;
> +}
> +
>  static int __send_signal_locked(int sig, struct kernel_siginfo *info,
>  				struct task_struct *t, enum pid_type type, bool force)
>  {
> @@ -1030,6 +1055,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;
>  
> @@ -1968,11 +1997,25 @@ static inline struct task_struct *posixt
>  	struct task_struct *t = pid_task(tmr->it_pid, tmr->it_pid_type);
>  
>  	if (t && tmr->it_pid_type != PIDTYPE_PID &&
> -	    same_thread_group(t, current) && !current->exit_state)
> +	    same_thread_group(t, current) && !(current->flags & PF_EXITING))
>  		t = current;
>  	return t;
>  }
>  
> +/*
> + * Find the target task for the POSIX timer signal and prevent that a
> + * PIDTYPE_PID signal is queued on a task which has PF_EXITING set.
> + */
> +static inline struct task_struct *posixtimer_get_unignore_target(struct k_itimer *tmr)
> +{
> +	struct task_struct *t = posixtimer_get_target(tmr);
> +
> +	if (t && task_can_queue_signal(t, tmr->it_pid_type))
> +		return t;
> +
> +	return NULL;
> +}
> +
>  void posixtimer_send_sigqueue(struct k_itimer *tmr)
>  {
>  	struct sigqueue *q = &tmr->sigq;
> @@ -1990,6 +2033,9 @@ void posixtimer_send_sigqueue(struct k_i
>  	if (!likely(lock_task_sighand(t, &flags)))
>  		return;
>  
> +	if (!task_can_queue_signal(t, tmr->it_pid_type))
> +		goto unlock;
> +
>  	/*
>  	 * Update @tmr::sigqueue_seq for posix timer signals with sighand
>  	 * locked to prevent a race against dequeue_signal().
> @@ -2081,6 +2127,7 @@ void posixtimer_send_sigqueue(struct k_i
>  	result = TRACE_SIGNAL_DELIVERED;
>  out:
>  	trace_signal_generate(sig, &q->info, t, tmr->it_pid_type != PIDTYPE_PID, result);
> +unlock:
>  	unlock_task_sighand(t, &flags);
>  }
>  
> @@ -2136,7 +2183,7 @@ static void posixtimer_sig_unignore(stru
>  		 * has exited by now, drop the reference count.
>  		 */
>  		guard(rcu)();
> -		target = posixtimer_get_target(tmr);
> +		target = posixtimer_get_unignore_target(tmr);
>  		if (target)
>  			posixtimer_queue_sigqueue(&tmr->sigq, target, tmr->it_pid_type);
>  		else
> @@ -3120,42 +3167,36 @@ static void retarget_shared_pending(stru
>  
>  void exit_signals(struct task_struct *tsk)
>  {
> +	LIST_HEAD(sigq_list);
>  	int group_stop = 0;
> -	sigset_t unblocked;
>  
>  	/*
>  	 * @tsk is about to have PF_EXITING set - lock out users which
> -	 * expect stable threadgroup.
> +	 * expect a stable threadgroup.
>  	 */
>  	cgroup_threadgroup_change_begin(tsk);
>  
> -	if (thread_group_empty(tsk) || (tsk->signal->flags & SIGNAL_GROUP_EXIT)) {
> +	scoped_guard(spinlock_irq, &tsk->sighand->siglock) {
>  		tsk->flags |= PF_EXITING;
> -		cgroup_threadgroup_change_end(tsk);
> -		return;
> -	}
>  
> -	spin_lock_irq(&tsk->sighand->siglock);
> -	/*
> -	 * From now this task is not visible for group-wide signals,
> -	 * see wants_signal(), do_signal_stop().
> -	 */
> -	tsk->flags |= PF_EXITING;
> +		sigqueue_dequeue_pending(&tsk->pending, &sigq_list);
>  
> -	cgroup_threadgroup_change_end(tsk);
> +		if (task_sigpending(tsk) && !thread_group_empty(tsk) &&
> +		    !(tsk->signal->flags & SIGNAL_GROUP_EXIT)) {
> +			sigset_t unblocked = tsk->blocked;
> +
> +			signotset(&unblocked);
> +			retarget_shared_pending(tsk, &unblocked);
> +
> +			if (unlikely(tsk->jobctl & JOBCTL_STOP_PENDING) &&
> +			    task_participate_group_stop(tsk))
> +				group_stop = CLD_STOPPED;
> +		}
> +	}
>  
> -	if (!task_sigpending(tsk))
> -		goto out;
> +	cgroup_threadgroup_change_end(tsk);
>  
> -	unblocked = tsk->blocked;
> -	signotset(&unblocked);
> -	retarget_shared_pending(tsk, &unblocked);
> -
> -	if (unlikely(tsk->jobctl & JOBCTL_STOP_PENDING) &&
> -	    task_participate_group_stop(tsk))
> -		group_stop = CLD_STOPPED;
> -out:
> -	spin_unlock_irq(&tsk->sighand->siglock);
> +	flush_sigqueue_list(&sigq_list);

It probably doesn't matter in practice, I don't know feel free to ignore,
but FWIW it looks like it's still vulnerable to the theoretical far fetched
race I described. The head is moved under the lock but individual nodes are
deleted without the lock.

CPU 0                                CPU 1                   CPU 2
-----                                -----                   -----

exit_signals()
   spin_lock(sighand)
   tsk->flags |= PF_EXITING;
   list_splice_init(&queue->list, head);
   spin_unlock(sighand)

   list_for_each_safe(head, node)
      list_del_init(node)
         node->next = node // A
         node->prev = node // B
...
                                     de_thread()
                                        // acquired tsk->flags
                                        // and signal flushed
                                        // through tasklist_lock
                                        transfer_pid() // C
                                                           
                                                           posix_timer_fn()
                                                              posixtimer_send_sigqueue()
                                                                 // OBSERVES C
                                                                 t = posixtimer_get_target(tmr)
                                                                 lock_task_sighand()
                                                                    // OBSERVES A
                                                                    if (!list_empty(q))
                                                                       // BUT NOT B
                                                                       list_add_tail(q) // D

Then who knows which write wins, B or D?

Thanks.

-- 
Frederic Weisbecker
SUSE Labs

  parent reply	other threads:[~2026-09-07 12:31 UTC|newest]

Thread overview: 50+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-05 18:58 [patch V2 0/8] exec/exit: POSIX timer related bugfixes and related cleanups Thomas Gleixner
2026-09-05 18:59 ` [patch V2 1/8] signal: Prevent exec() race Thomas Gleixner
2026-09-06 13:17   ` Oleg Nesterov
2026-09-06 22:39   ` Eric W. Biederman
2026-09-06 23:28     ` Oleg Nesterov
2026-09-07 11:26     ` Thomas Gleixner
2026-09-07 12:31   ` Frederic Weisbecker [this message]
2026-09-07 15:26     ` Thomas Gleixner
2026-09-07 20:15       ` Frederic Weisbecker
2026-09-07 22:28         ` Thomas Gleixner
2026-09-08 10:15           ` Frederic Weisbecker
2026-09-09  0:03             ` Oleg Nesterov
2026-09-09  9:17               ` Frederic Weisbecker
2026-09-09  8:04             ` Peter Zijlstra
2026-09-09  9:08               ` Thomas Gleixner
2026-09-09  9:55                 ` Peter Zijlstra
2026-09-09 10:20                   ` Peter Zijlstra
2026-09-09 11:31                   ` Thomas Gleixner
2026-09-09 12:13                   ` Frederic Weisbecker
2026-09-09 12:45                     ` Peter Zijlstra
2026-09-09 12:51                       ` Peter Zijlstra
2026-09-09 13:45                         ` Thomas Gleixner
2026-09-09 15:48                           ` Frederic Weisbecker
2026-09-09 16:00                           ` Frederic Weisbecker
2026-09-09 14:33                       ` Alan Stern
2026-09-09 14:45                       ` Frederic Weisbecker
2026-09-09 19:28                         ` Alan Stern
2026-09-09 20:49                           ` Thomas Gleixner
2026-09-09 21:11                             ` Alan Stern
2026-09-10 13:21                               ` Frederic Weisbecker
2026-09-10 13:28                                 ` Peter Zijlstra
2026-09-10 15:26                                 ` Alan Stern
2026-09-09 10:18                 ` Frederic Weisbecker
2026-09-09  9:11               ` Frederic Weisbecker
2026-09-05 18:59 ` [patch V2 2/8] exec: Cleanup POSIX timers right after de_thread() Thomas Gleixner
2026-09-06 13:21   ` Oleg Nesterov
2026-09-07 22:13   ` Frederic Weisbecker
2026-09-05 18:59 ` [patch V2 3/8] posix-timers: Move posixtimer_exec_cleanup() out of exec.c Thomas Gleixner
2026-09-10 13:50   ` Frederic Weisbecker
2026-09-05 18:59 ` [patch V2 4/8] posix-timers: Move POSIX timer group exit related code out of do_exit() Thomas Gleixner
2026-09-10 13:59   ` Frederic Weisbecker
2026-09-05 18:59 ` [patch V2 5/8] posix-cpu-timers: Move inlines out of public header Thomas Gleixner
2026-09-10 14:00   ` Frederic Weisbecker
2026-09-05 18:59 ` [patch V2 6/8] posix-cpu-timers: Use PF_EXITING to indicate exit Thomas Gleixner
2026-09-05 18:59 ` [patch V2 7/8] posix-cpu-timers: Prevent enqueueing when PF_EXITING is set Thomas Gleixner
2026-09-06 16:26   ` Oleg Nesterov
2026-09-07 12:20     ` Thomas Gleixner
2026-09-05 18:59 ` [patch V2 8/8] posix-timers: Handle exit in do_exit() completely Thomas Gleixner
2026-09-06 16:40   ` Oleg Nesterov
2026-09-07 12:27     ` Thomas Gleixner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ap6urw5aAxrKO3Ck@localhost.localdomain \
    --to=frederic@kernel.org \
    --cc=brauner@kernel.org \
    --cc=ebiederm@xmission.com \
    --cc=imv4bel@gmail.com \
    --cc=jstultz@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mingo@kernel.org \
    --cc=oleg@redhat.com \
    --cc=peterz@infradead.org \
    --cc=stable@vger.kernel.org \
    --cc=tglx@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®