This is the mail archive of the
libc-alpha@sourceware.org
mailing list for the glibc project.
Race in pthread_mutex_lock while promoting to PTHREAD_MUTEX_ELISION_NP.
- From: Stefan Liebler <stli at linux dot vnet dot ibm dot com>
- To: GNU C Library <libc-alpha at sourceware dot org>
- Date: Thu, 24 May 2018 16:39:16 +0200
- Subject: Race in pthread_mutex_lock while promoting to PTHREAD_MUTEX_ELISION_NP.
Hi,
while looking for a possible reason for "pthread_mutex_lock.c:79:
__pthread_mutex_lock: Assertion `mutex->__data.__owner == 0' failed.",
I've found a race condition if pthread_mutex_lock() is called for the
"first time" by multiple threads.
Note: Lock elision has to be supported and enabled (export
GLIBC_TUNABLES=glibc.elision.enable=1).
The attached test program creates threads which runs multiple iterations
of the following sequence:
-pthread_mutex_init(): called only by one thread. Initializing with
default mutex attributes.
-pthread_mutex_lock()
-pthread_mutex_unlock()
-pthread_mutex_destroy(): called only by one thread.
Here are some code snippets in order to follow the explanation below:
-nptl/pthread_mutex_lock.c:
62int
63__pthread_mutex_lock (pthread_mutex_t *mutex)
64{
65 unsigned int type = PTHREAD_MUTEX_TYPE_ELISION (mutex);
...
73 if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_NP))
74 {
75 FORCE_ELISION (mutex, goto elision);
76 simple:
77 /* Normal mutex. */
78 LLL_MUTEX_LOCK (mutex);
79 assert (mutex->__data.__owner == 0);
80 }
81#ifdef HAVE_ELISION
82 else if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_ELISION_NP))
83 {
84 elision: __attribute__((unused))
85 /* This case can never happen on a system without elision,
86 as the mutex type initialization functions will not
87 allow to set the elision flags. */
88 /* Don't record owner or users for elision case. This is a
89 tail call. */
90 return LLL_MUTEX_LOCK_ELISION (mutex);
91 }
92#endif
...
156 /* Record the ownership. */
157 mutex->__data.__owner = id;
158#ifndef NO_INCR
159 ++mutex->__data.__nusers;
160#endif
-sysdeps/unix/sysv/linux/s390/force-elision.h:
Note: This file is identical to the equivalent files for x86 and powerpc!
19/* Automatically enable elision for existing user lock kinds. */
20#define FORCE_ELISION(m, s) \
21 if (__pthread_force_elision \
22 && (m->__data.__kind & PTHREAD_MUTEX_ELISION_FLAGS_NP) == 0) \
23 { \
24 mutex->__data.__kind |= PTHREAD_MUTEX_ELISION_NP; \
25 s; \
26 }
-nptl/pthread_mutex_unlock.c:
36__pthread_mutex_unlock_usercnt (pthread_mutex_t *mutex, int decr)
37{
38 int type = PTHREAD_MUTEX_TYPE_ELISION (mutex);
...
43 if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)
44 == PTHREAD_MUTEX_TIMED_NP)
45 {
46 /* Always reset the owner field. */
47 normal:
48 mutex->__data.__owner = 0;
49 if (decr)
50 /* One less user. */
51 --mutex->__data.__nusers;
...
60 else if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_ELISION_NP))
61 {
62 /* Don't reset the owner/users fields for elision. */
63 return lll_unlock_elision (mutex->__data.__lock,
mutex->__data.__elision,
64 PTHREAD_MUTEX_PSHARED (mutex));
-nptl/pthread_mutex_destroy.c:
26__pthread_mutex_destroy (pthread_mutex_t *mutex)
27{
28 LIBC_PROBE (mutex_destroy, 1, mutex);
29
30 if ((mutex->__data.__kind & PTHREAD_MUTEX_ROBUST_NORMAL_NP) == 0
31 && mutex->__data.__nusers != 0)
32 return EBUSY;
Assumption: Two threads are calling pthread_mutex_lock at the same time
and have already loaded the mutex-type in line 65 with the type
PTHREAD_MUTEX_TIMED_NP.
Thread 1 promotes the mutex to PTHREAD_MUTEX_ELISION_NP (see
FORCE_ELISION in line 24) and jumps to LLL_MUTEX_LOCK_ELISION (see line 90).
Thread 2 is checking the mutex type in FORCE_ELISION (line 22).
As the mutex-type is already set to PTHREAD_MUTEX_ELISION_NP, the
condition is false and the "Normal mutex" (line 77) is processed.
Note: The "normal mutex" records the ownership (see line 156).
Both threads are calling pthread_mutex_unlock and are loading mutex-type
in line 38 with the type PTHREAD_MUTEX_TIMED_ELISION_NP.
Thus the ownership is not resetted (see line 62)!
The call to pthread_mutex_destroy is returning with EBUSY
as __nusers == 1 (see line 31).
Note: Although thread 2 has successfully called pthread_mutex_unlock, it
is marked as the current owner of the mutex.
If a third thread has also loaded the mutex-type PTHREAD_MUTEX_TIMED_NP
in pthread_mutex_lock, then the "assert (mutex->__data.__owner == 0)" in
line 79 is triggered.
When I am running the attached test program on s390x, I can trigger the
case "pthread_mutex_destroy()==EBUSY" within some thousand iterations.
And I could trigger the assertion while single-stepping in GDB.
Can somebody test and confirm this on x86, power, s390?
How can we solve this issue?
Bye
Stefan
//CFLAGS=
//LDFLAGS=-lpthread
#define CHECK_LOCK_ELISION 1
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#if CHECK_LOCK_ELISION != 0
# ifdef __s390x__
# include <sys/auxv.h>
# include <htmintrin.h> /* Needed for builtin_tx_nesting_depth. */
# endif
#endif
#define NUM_THREADS 3
typedef struct thr_info
{
int nr;
pthread_t thread;
} thr_info_t;
static pthread_barrier_t barrier;
static pthread_mutex_t mutex;
static int loop_count = -1;
static void
check_fail (const char *msg, thr_info_t *thr, int ret, int *info_round)
{
if (ret != 0)
{
printf ("#%d: %s: failed with %d;",
(thr == NULL) ? -1 : thr->nr, msg, ret);
if (info_round != NULL)
printf (" in round=%d;", *info_round);
puts ("");
abort ();
}
}
static void *
thr_func (void *arg)
{
int ret, i;
thr_info_t *thr = (thr_info_t *) arg;
printf ("#%d: started\n", thr->nr);
for (i = 0; i < loop_count; i++)
{
if (thr->nr == 0)
{
ret = pthread_mutex_init (&mutex, NULL);
check_fail ("pthread_mutex_init", thr, ret, &i);
}
pthread_barrier_wait (&barrier);
ret = pthread_mutex_lock (&mutex);
check_fail ("pthread_mutex_lock", thr, ret, &i);
ret = pthread_mutex_unlock (&mutex);
check_fail ("pthread_mutex_unlock", thr, ret, &i);
pthread_barrier_wait (&barrier);
if (thr->nr == 0)
{
ret = pthread_mutex_destroy (&mutex);
check_fail ("pthread_mutex_destroy", thr, ret, &i);
if ((i % 10000) == 0)
{
printf (".");
fflush (stdout);
}
}
pthread_barrier_wait (&barrier);
}
return NULL;
}
#if CHECK_LOCK_ELISION != 0
static int
check_if_lock_elision_is_available ()
{
int success = EXIT_FAILURE;
# ifdef __s390x__
if ((getauxval (AT_HWCAP) & HWCAP_S390_TE) == 0)
{
puts ("This machine does not support transactional execution!\n"
"You need a s390x CPU >= zEC12 and a lpar or a z/VM guest which "
"supports transactions!\n");
}
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
int nesting_depth_in_lock = -1;
int ret;
ret = pthread_mutex_lock (&m);
check_fail ("check-elision; lock", NULL, ret, NULL);
__asm__ (".machine push \n\t"
".machine \"all\"");
nesting_depth_in_lock = __builtin_tx_nesting_depth ();
__asm__ (".machine pop");
ret = pthread_mutex_unlock (&m);
check_fail ("check-elision; unlock", NULL, ret, NULL);
if (nesting_depth_in_lock > 0)
{
puts ("The lock was elided!");
success = EXIT_SUCCESS;
}
else
{
puts ("The lock was NOT elided!\n"
"Perhaps you've forgot to set the environment variable:\n"
"GLIBC_TUNABLES=glibc.elision.enable=1");
}
# else
puts ("Please add a check if lock-elision is available on your architecture. "
"The check in check_if_lock_elision_is_available () assumes, "
"that lock-elision is enabled!\n\n");
success = EXIT_SUCCESS;
# endif
return success;
}
#endif
int
main (int argc, const char *argv[])
{
int i;
int ret;
if (argc == 2)
{
loop_count = strtol (argv[1], NULL, 0);
}
if (loop_count <= 0)
loop_count = 2000000;
#if CHECK_LOCK_ELISION != 0
check_if_lock_elision_is_available ();
#endif
printf ("main: start %d threads to run %d iterations.\n",
NUM_THREADS, loop_count);
ret = pthread_barrier_init (&barrier, NULL, NUM_THREADS);
check_fail ("pthread_barrier_init", NULL, ret, NULL);
thr_info_t thrs[NUM_THREADS];
for (i = 0; i < NUM_THREADS; i++)
{
thrs[i].nr = i;
ret = pthread_create (&(thrs[i].thread), NULL, thr_func, &(thrs[i]));
check_fail ("pthread_create", NULL, ret, NULL);
}
for (i = 0; i < NUM_THREADS; i++)
{
ret = pthread_join (thrs[i].thread, NULL);
check_fail ("pthread_join", NULL, ret, NULL);
}
ret = pthread_barrier_destroy (&barrier);
check_fail ("pthread_barrier_destroy", NULL, ret, NULL);
printf ("main: end.\n");
return EXIT_SUCCESS;
}