I've come across some unexpected behaviour with pthread_detach
. Consider the following code:
#include <pthread.h>
int main() {
pthread_detach(0);
}
It's obviously an error (0 is not a valid pthread_t
). When I run this code, the process crashes with a segfault.
The problem is that, according to the man page, it should handle this error and simply return ESRCH (since this should just be a case of "no thread with ID 0 found", right?)
The only undefined behaviour mentioned by the man page is when attempting to detach an already detached thread, which this isn't.
I'm using glibc version 2.39-4.2; could this be a bug?
Update: I've found the code responsible for this, here: https://elixir.bootlin.com/glibc/glibc-2.39/source/nptl/pthread_detach.c#L29
So it seems that "thread ID" in the man page refers to some property (tid, see here) of the thread structure (which is pointed to by the pthread_t
type. This explains the segfault, but I'm still not convinced this is expected behaviour.
Update 2: I just came across this very related question, which seems to pretty much answer this one.
As you tagged your question with glibc
, the answer is in the GLIBc source code. The source file is nptl/pthread_detach.c:
int
pthread_detach (pthread_t th)
{
struct pthread *pd = (struct pthread *) th;
/* Make sure the descriptor is valid. */
if (INVALID_NOT_TERMINATED_TD_P (pd))
/* Not a valid thread handle. */
return ESRCH;
int result = 0;
/* Mark the thread as detached. */
if (atomic_compare_and_exchange_bool_acq (&pd->joinid, pd, NULL))
{
/* There are two possibilities here. First, the thread might
already be detached. In this case we return EINVAL.
Otherwise there might already be a waiter. The standard does
not mention what happens in this case. */
if (IS_DETACHED (pd))
result = EINVAL;
}
else
/* Check whether the thread terminated meanwhile. In this case we
will just free the TCB. */
if ((pd->cancelhandling & EXITING_BITMASK) != 0)
/* Note that the code in __free_tcb makes sure each thread
control block is freed only once. */
__free_tcb (pd);
return result;
}
The parameter passed to the API is checked with INVALID_NOT_TERMINATED_TD_P (pd)
. If the check fails, ESRCH is returned but the latter is defined in nptl/pthreadP.h as:
/* Simplified test. This will not catch all invalid descriptors but
is better than nothing. And if the test triggers the thread
descriptor is guaranteed to be invalid. */
# define INVALID_TD_P(pd) __builtin_expect ((pd)->tid <= 0, 0)
# define INVALID_NOT_TERMINATED_TD_P(pd) __builtin_expect ((pd)->tid < 0, 0)
The above macro does not check if the parameter is valid (at least not equal to 0) to dereference it through (pd)->tid
. This is why you get a crash.
The ESRCH error can appear when you use the thread identifier of a terminated thread because internally the GLIBC keeps the stacks of the last terminated threads into an internal pool to be able to reallocate them quickly to future threads. The thread identifier is actually a pointer on the thread TCB located in its stack. Of course, the thread descriptor is marked "unused" for future existence checks.
The following example creates a thread and waits for its termination through pthread_join()
. Then, pthread_detach()
is called with the thread identifier of the terminated thread:
#include <pthread.h>
#include <errno.h>
#include <stdio.h>
extern int pthread_detach (pthread_t __th);
void *th_main(void *p)
{
printf("Thread is running\n");
return NULL;
}
int main() {
int rc;
pthread_t tid;
// Default attribute: joinable
rc = pthread_create(&tid, NULL, th_main, NULL);
if (rc != 0) {
errno = rc;
fprintf(stderr, "pthread_create(): rc = '%m' (%d)\n", rc);
return 1;
}
printf("Created thread %p\n", (void *)tid);
rc = pthread_join(tid, NULL);
if (rc != 0) {
errno = rc;
fprintf(stderr, "pthread_join(): rc = '%m' (%d)\n", rc);
return 1;
}
rc = pthread_detach(tid);
if (rc != 0) {
errno = rc;
fprintf(stderr, "pthread_detach(%p): rc = '%m' (%d)\n", (void *)tid, rc);
return 1;
}
return 0;
}
The execution shows that the call to pthread_detach()
on the terminated thread returns ESRCH:
$ ./a.out
Created thread 0x7f671dcb7700
Thread is running
pthread_detach(0x7f671dcb7700): rc = 'No such process' (3)