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

From: Thomas Gleixner

Date: Wed Sep 09 2026 - 05:27:25 EST


On Wed, Sep 09 2026 at 10:04, Peter Zijlstra wrote:
> On Tue, Sep 08, 2026 at 12:15:21PM +0200, Frederic Weisbecker wrote:
> Let me try and have a go :-)
>
>
> do_exit() de_thread() posix_timer_fn()
> exit_signal() LOCK siglock posix_timer_send_sigqueue()
> LOCK siglock UNLOCK siglock t = posix_timer_get_target()
> tsk->flags |= PF_EXITING; LOCK siglock
> UNLOCK siglock if (!thread_group_leader) if (!list_empty(sigqueue))
> LOCK tasklist_lock
> flush_sigqueue_list(); if (leader->exit_state)
> break;
> ... transfer_pid()
> UNLOCK tasklist_lock
> exit_notify()
> LOCK tasklist_lock
> tsk->exit_state = EXIT_ZOMBIE;
> UNLOCK tasklist_lock
>
>
>
> Then there is indeed nothing that makes sure posix_timer_fn() sees
> sigqueue updates done by do_exit(), because those are ordered by
> tasklist_lock, but posix_timer_fn() doesn't care about that.

That's irrelevant because in the above scenario posix_timer_fn() 't'
points to the exiting old leader (on the left) because the PID store has
not happened yet and it therefore observes PF_EXITING on it so it won't
touch the sigqueue. Note, that setting and checking PF_EXITING is
serialized by sighand lock, so this is fine.

do_exit()
exit_signals()
LOCK siglock
tsk->flags |= PF_EXITING
UNLOCK siglock

So after this point anything which looks at tsk->flags under siglock
will observe PF_EXITING and not touch the sigqueue. Nothing to see here.

> The easy solution would probably be to do transfer_pid() while holding
> siglock?

That'd be only relevant for the situation Frederic is concerned about,
i.e. the case where the third party observes the TID swap.

Because with that visible 't' in posix_timer_send_sigqueue() won't be
old_leader, which has PF_EXITING set, it will be new_leader which has it
not set.

So Frederic is concerned that posix_timer_send_sigqueue() can observe
the PID store but not observe the sigqueue stores.

I argue that's not possible:

A: sigqueue stores

B: AQUIRE tasklist

C: exit_state store

D: RELEASE tasklist
// sigqueue and exit_state stores become globally visible
------------------------------------------------------------------------

E ACQUIRE tasklist
------------------------------------------------------------------------
F if (exit_state)
swap_pid()
G STORE_PID

// The PID store can become visible in the
// system right here so F can observe them before
// RELEASE tasklist

H READ PID
....
I ACQUIRE siglock

After #A the sigqueue stores are maybe visible

After #C the exit_state store is maybe visible

After #D both #A and #C are guaranteed to be visible to _ALL_ agents in
the system and cannot become magically become invisible after that
point.

The new leader cannot swap PIDs before acquiring task list lock and
before it observed exit_state != 0 under it. That's fully serialized
against the old leader as both hold task list lock for their operations.

#F creates a control dependency, so if the new leader acquires task list
lock before the old it will observe 0, drop the lock and wait. No PID
store obviously.

#G can be come visible immediately but is only guaranteed to be visible
globally at the RELEASE of tasklist lock.

#H can only observe the PID store after the store actually happened in
#G. So it either reads the original PID or the swapped PID.

#I is not really relevant for this. It's only relevant for PF_EXITING
and other stuff which is directly protected by it. And it does not
matter whether it locks the old or the new sighand.

Now let's look at the full chain and what can possibly be visible or not
and when:

#A can trickle into the tasklist held section, but not after #D.

#C cannot be reordered against #B and #D

#A is therefore guaranteed to be globally visible _before_ new leader
observes exit_state != 0 in #F under task list lock

#G cannot be reordered against #F and obviously not against #E either.

It can become visible at any point after the store, but as argued
above that visibility can't be reordered before #A (sigqueue stores)
became visible.

The important part is that the visibility of #A (sigqueue stores) and #G
(PID store) is fully ordered through task list lock.

So #H _cannot_ observe #G without observing #A - not even on PowerPC or
similar insanities.

No?

Also doing the PID swap under sighand lock is not solving anything
either because posix_timer_send_sigqueue() does the lookup without the
lock simply because it does not know which task it is upfront. So it
would have to redo and validate the lookup with the lock held.

Thanks,

tglx