[PATCH v5 2/3] nptl: Do not use pthread set_tid_address as state synchronization (BZ #19951)
Florian Weimer
fweimer@redhat.com
Wed Dec 3 18:10:15 GMT 2025
* Adhemerval Zanella Netto:
>>> + unsigned int prevstate;
>>> + do
>>> + prevstate = atomic_load_relaxed (&pd->joinstate);
>>> + while (!atomic_compare_exchange_weak_acquire (&pd->joinstate, &prevstate,
>>> + THREAD_STATE_EXITING));
>>> +
>>
>> Why acquire MO? I think even if the kernel uses a release store, it's
>> not exactly clear what we are synchronizing with.
>>
>> And this looks like an unconditional atomic_exchange.
>
> My understanding here in since this code compete with the
> pthread_detach (issued by a different thread), the CAS is need to
> determine which is one responsible to free the TCB: either the thread
> itself (below with __nptl_free_tcb), or pthread_detach with
> __pthread_join() call.
>
> I am not sure about using an unconditional atomic_exchange, it means
> will need to change pthread_detach() to ignore THREAD_STATE_EXITING
> state and call __nptl_free_tcb unconditionally. And ignoring
> THREAD_STATE_EXITINGT makes the sysdeps/pthread/tst-detach1.c usage
> tricky to handle.
But isn't the loop above an unconditional exchange? I thought that
atomic_compare_exchange_weak_acquire helpfully updates prevstate to the
value after the failed compare. If you don't want to overwrite other
states, you need to re-check prevstate before retrying the CAS.
> And I use the acquire because this is the current practice for concurrent
> implementations, like __new_sem_wait_fast. But I am not really an concurrency
> expert so I am not fully sure if this is the correct MO here.
Yeah, I found this concurrency stuff confusing as well.
>>> @@ -706,7 +725,9 @@ __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
>>> /* Initialize the field for the ID of the thread which is waiting
>>> for us. This is a self-reference in case the thread is created
>>> detached. */
>>> - pd->joinid = iattr->flags & ATTR_FLAG_DETACHSTATE ? pd : NULL;
>>> + pd->joinstate = iattr->flags & ATTR_FLAG_DETACHSTATE
>>> + ? THREAD_STATE_DETACHED
>>> + : THREAD_STATE_JOINABLE;
>>
>> There's also an assignment in create_thread above.
>
> Do mean the '.child_tid = (uintptr_t) &pd->joinstate,' in clone setup?
> If so, my undertanding is kernel will only set the value at thread
> termination.
I left that in my review by mistake, the referenced assignment was for
the failed case only, so not actually redundant.
>> Can we put in an assert that checks for prevstate ==
>> PTHREAD_STATE_EXITING || prevstate == PTHREAD_STATE_EXITED? Or would
>> that reveal to many bugs in existing applications?
>
> I am not sure, I tend to avoid adding assert on pthread functions because we
> do support some UB (as defined by POSIX) so we can't guarantee that programs
> do not rely on such semantics.
Okay.
>>> diff --git a/nptl/pthread_join_common.c b/nptl/pthread_join_common.c
>>> index 9109b62276..f0ae5edbcb 100644
>>> --- a/nptl/pthread_join_common.c
>>> +++ b/nptl/pthread_join_common.c
>>> @@ -22,116 +22,78 @@
>>> #include <time.h>
>>> #include <futex-internal.h>
>>>
>>> +/* Check for a possible deadlock situation where the threads are waiting for
>>> + each other to finish. Note that this is a "may" error. To be 100% sure we
>>> + catch this error we would have to lock the data structures but it is not
>>> + necessary. In the unlikely case that two threads are really caught in this
>>> + situation they will deadlock. It is the programmer's problem to figure
>>> + this out. */
>>> +static inline bool
>>> +check_for_deadlock (struct pthread *pd)
>>> {
>>> + struct pthread *self = THREAD_SELF;
>>> + return ((pd == self
>>> + || (atomic_load_acquire (&self->joinstate) == THREAD_STATE_DETACHED
>>> + && (pd->cancelhandling
>>> + & (CANCELING_BITMASK | CANCELED_BITMASK | EXITING_BITMASK
>>> + | TERMINATED_BITMASK)) == 0))
>>> + && !cancel_enabled_and_canceled (self->cancelhandling));
>>> }
>>
>> Why acquire MO?
>>
>> Is the condition really correct (beyond pd ==self)? Why is
>> self->joinstate == THREAD_STATE_DETACHED treated differently here?
>>
>> I think with pd->joinid gone, there really isn't much we can check here
>> anymore. Maybe just stick to pd == self and do away with this separate
>> function altogether?
>
> Ack, I am not really found of this 'may' deadlock check and unfortunately
> now that is backed on glibc semantic I did not want to changed it too
> much.
Sorry, what do you mean?
>>> int
>>> __pthread_clockjoin_ex (pthread_t threadid, void **thread_return,
>>> clockid_t clockid,
>>> + const struct __timespec64 *abstime)
>>
>>> + int result = 0;
>>> + unsigned int state;
>>> + while ((state = atomic_load_acquire (&pd->joinstate))
>>> + != THREAD_STATE_EXITED)
>>> {
>>> + if (check_for_deadlock (pd))
>>> + return EDEADLK;
>>> +
>>> + /* POSIX states calling pthread_join on a non joinable thread is
>>> + undefined. However, if PD is still in the cache we can warn
>>> + the caller. */
>>> + if (state == THREAD_STATE_DETACHED)
>>> + return EINVAL;
>>> +
>>> + /* pthread_join is a cancellation entrypoint and we use the same
>>> + rationale for pthread_timedjoin_np.
>>> +
>>> + The kernel notifies a process which uses CLONE_CHILD_CLEARTID via
>>> + a memory zeroing and futex wake-up when the process terminates.
>>> + The futex operation is not private. */
>>> + int ret = __futex_abstimed_wait_cancelable64 (&pd->joinstate, state,
>>> + clockid, abstime,
>>> + LLL_SHARED);
>>> + if (ret == ETIMEDOUT || ret == EOVERFLOW)
>>> + {
>>> + result = ret;
>>> + break;
>>> }
>>> }
>>>
>>> void *pd_result = pd->result;
>>> if (__glibc_likely (result == 0))
>>> {
>>> if (thread_return != NULL)
>>> *thread_return = pd_result;
>>>
>>> /* Free the TCB. */
>>> __nptl_free_tcb (pd);
>>> }
>>
>> I'm trying to understand if this leaks the TCB on cancellation. As far
>> as I can tell, phtread_join needs to be called again if it gets
>> canceled, so this should be okay.
>>
>> For synchronization purposes, rather than relying on acquire loads on
>> joinstate (where the kernel may or may not perform a release store),
>> maybe we should use an acquire load on pd->result (and a corresponding
>> release store)?
>
> I am not sure, the 'joinstate' allows us to determine which is the thread
> responsible to free the TCB state; using 'result' will require additional
> synchronization and some extra care on its state to certify that we can
> assume which thread can actually free the TCB.
Sorry, I meant something else: We need to take steps so that the return
from the thread start routine (or the pthread_exit call) synchronizes
with the pthread_join call. Doing this via joinstate only works if the
kernel performs a release store (which isn't specified in the clone(2)
manual page). We could do a release store on pd->result ourselves and
an acquire load from pd->result in the thread calling pthread_join.
But this is a distraction because we've always relayed on this CLEARTID
release semantics. The change here is just the name of the field.
>>> diff --git a/nptl/pthread_tryjoin.c b/nptl/pthread_tryjoin.c
>>> index 54b528fd19..e9a10aee53 100644
>>> --- a/nptl/pthread_tryjoin.c
>>> +++ b/nptl/pthread_tryjoin.c
>>> @@ -21,15 +21,18 @@
>>> int
>>> __pthread_tryjoin_np (pthread_t threadid, void **thread_return)
>>> {
>>> + /* The joinable state (THREAD_STATE_JOINABLE) is straightforward: the thread
>>> + hasn't finished yet, so trying to join might block.
>>> +
>>> + The exiting thread (THREAD_STATE_EXITING) also might result in a blocking
>>> + call: a detached thread might change its state to exiting, and an exiting
>>> + thread might take some time to exit (and thus let the kernel set the
>>> + state to THREAD_STATE_EXITED). */
>>>
>>> + struct pthread *pd = (struct pthread *) threadid;
>>> + return atomic_load_acquire (&pd->joinstate) != THREAD_STATE_EXITED
>>> + ? EBUSY
>>> + : __pthread_clockjoin_ex (threadid, thread_return, 0, NULL);
>>> }
>>> versioned_symbol (libc, __pthread_tryjoin_np, pthread_tryjoin_np, GLIBC_2_34);
>>
>> Pre-existing issue: pthread_tryjoin_np cannot be a cancellation point
>> because it is declared _THROW.
>
> Before the patch we have the 'block' argument o specify whether to call
> __futex_abstimed_wait_cancelable64, now if __pthread_clockjoin_ex is called
> 'joinstate' is assumed to be THREAD_STATE_EXITED.
>
> The pthread_detach will only change the 'joinstate' if previous state iff
> THREAD_STATE_JOINABLE, so even concurrent pthread_detach will not change this
> invariant.
Ahh, so you are saying we never call __futex_abstimed_wait_cancelable64?
Thanks,
Florian
More information about the Libc-alpha
mailing list