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

From: Frederic Weisbecker

Date: Wed Sep 09 2026 - 06:25:42 EST


Le Wed, Sep 09, 2026 at 11:08:31AM +0200, Thomas Gleixner a écrit :
> 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?

I've always been told that ordering only works if paired.
But in practice I must confess I don't know much about hardware details.

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

It does the lookup without the lock but if pid is rewritten inside
the siglock on the write side and we observe the new pid from read side, then
acquiring the lock afterwards on the read side also acquires what it has
released previously (that is, everything that was acquired by tasklist_lock,
including the list_del_init()).

Not sure if my words are clear but tools/memory-model/litmus-tests/MP+polocks.litmus
explains that better.

Thanks.

--
Frederic Weisbecker
SUSE Labs