diff mbox series

[v2] stdlib: Allow concurrent quick_exit (BZ 31997)

Message ID 20240805142758.806079-1-adhemerval.zanella@linaro.org
State New
Headers show
Series [v2] stdlib: Allow concurrent quick_exit (BZ 31997) | expand

Commit Message

Adhemerval Zanella Netto Aug. 5, 2024, 2:27 p.m. UTC
As for exit, also allows concurrent quick_exit to avoid race
conditions when it is called concurrently.  Since it uses the same
internal function as exit, the __exit_lock lock is moved to
__run_exit_handlers.  It also solved a potential concurrent when
calling exit and quick_exit concurrently.

The test case 'expected' is expanded to a value larger than the
minimum required by C/POSIX (32 entries) so at_quick_exit() will
require libc to allocate a new block.  This makes the test mre likely to
trigger concurrent issues (through free() at __run_exit_handlers)
if quick_exit() interacts with the at_quick_exit list concurrently.

This is also the latest interpretation of the Austin Ticket [1].

Checked on x86_64-linux-gnu.

[1] https://austingroupbugs.net/view.php?id=1845
--
Changes from v1:
- Fixed lint-makefiles issue.
---
 stdlib/Makefile                       |   1 +
 stdlib/exit.c                         |  18 +--
 stdlib/tst-concurrent-exit-skeleton.c | 160 ++++++++++++++++++++++++++
 stdlib/tst-concurrent-exit.c          | 141 +----------------------
 stdlib/tst-concurrent-quick_exit.c    |  22 ++++
 5 files changed, 196 insertions(+), 146 deletions(-)
 create mode 100644 stdlib/tst-concurrent-exit-skeleton.c
 create mode 100644 stdlib/tst-concurrent-quick_exit.c

Comments

Carlos O'Donell Aug. 5, 2024, 4:06 p.m. UTC | #1
On 8/5/24 10:27 AM, Adhemerval Zanella wrote:
> As for exit, also allows concurrent quick_exit to avoid race
> conditions when it is called concurrently.  Since it uses the same
> internal function as exit, the __exit_lock lock is moved to
> __run_exit_handlers.  It also solved a potential concurrent when
> calling exit and quick_exit concurrently.

Do we also have the same problem with dlclose() which is going to possibly remove
an entire object along with registered functions?

Should dlclose() take the same recursive lock such that a developer either sees
the dlclose() happen first, followed by exit/quick_exit, or not see it at all?

> The test case 'expected' is expanded to a value larger than the
> minimum required by C/POSIX (32 entries) so at_quick_exit() will
> require libc to allocate a new block.  This makes the test mre likely to
> trigger concurrent issues (through free() at __run_exit_handlers)
> if quick_exit() interacts with the at_quick_exit list concurrently.
> 
> This is also the latest interpretation of the Austin Ticket [1].

LGTM. Though we may need a follow-up dlclose change.

Reviewed-by: Carlos O'Donell <carlos@redhat.com>

> 
> Checked on x86_64-linux-gnu.
> 
> [1] https://austingroupbugs.net/view.php?id=1845
> --
> Changes from v1:
> - Fixed lint-makefiles issue.
> ---
>  stdlib/Makefile                       |   1 +
>  stdlib/exit.c                         |  18 +--
>  stdlib/tst-concurrent-exit-skeleton.c | 160 ++++++++++++++++++++++++++
>  stdlib/tst-concurrent-exit.c          | 141 +----------------------
>  stdlib/tst-concurrent-quick_exit.c    |  22 ++++
>  5 files changed, 196 insertions(+), 146 deletions(-)
>  create mode 100644 stdlib/tst-concurrent-exit-skeleton.c
>  create mode 100644 stdlib/tst-concurrent-quick_exit.c
> 
> diff --git a/stdlib/Makefile b/stdlib/Makefile
> index f659c38feb..043a452f72 100644
> --- a/stdlib/Makefile
> +++ b/stdlib/Makefile
> @@ -274,6 +274,7 @@ tests := \
>    tst-bz20544 \
>    tst-canon-bz26341 \
>    tst-concurrent-exit \
> +  tst-concurrent-quick_exit \

OK.

>    tst-cxa_atexit \
>    tst-environ \
>    tst-getrandom \
> diff --git a/stdlib/exit.c b/stdlib/exit.c
> index bbaf138806..8d7e2e53d0 100644
> --- a/stdlib/exit.c
> +++ b/stdlib/exit.c
> @@ -28,6 +28,13 @@
>     __exit_funcs_lock is declared.  */
>  bool __exit_funcs_done = false;
>  
> +/* The lock handles concurrent exit() and quick_exit(), even though the
> +   C/POSIX standard states that calling exit() more than once is UB.  The
> +   recursive lock allows atexit() handlers or destructors to call exit()
> +   itself.  In this case, the  handler list execution will resume at the
> +   point of the current handler.  */
> +__libc_lock_define_initialized_recursive (static, __exit_lock)

OK. Moved up from below and added note about quick_exit.

> +
>  /* Call all functions registered with `atexit' and `on_exit',
>     in the reverse of the order in which they were registered
>     perform stdio cleanup, and terminate program execution with STATUS.  */
> @@ -36,6 +43,9 @@ attribute_hidden
>  __run_exit_handlers (int status, struct exit_function_list **listp,
>  		     bool run_list_atexit, bool run_dtors)
>  {
> +  /* The exit should never return, so there is no need to unlock it.  */
> +  __libc_lock_lock_recursive (__exit_lock);

OK.

> +
>    /* First, call the TLS destructors.  */
>    if (run_dtors)
>      call_function_static_weak (__call_tls_dtors);
> @@ -132,17 +142,9 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
>  }
>  
>  
> -/* The lock handles concurrent exit(), even though the C/POSIX standard states
> -   that calling exit() more than once is UB.  The recursive lock allows
> -   atexit() handlers or destructors to call exit() itself.  In this case, the
> -   handler list execution will resume at the point of the current handler.  */
> -__libc_lock_define_initialized_recursive (static, __exit_lock)
> -

OK.

>  void
>  exit (int status)
>  {
> -  /* The exit should never return, so there is no need to unlock it.  */
> -  __libc_lock_lock_recursive (__exit_lock);

OK. Moved into the handling of exit handlers.

>    __run_exit_handlers (status, &__exit_funcs, true, true);
>  }
>  libc_hidden_def (exit)
> diff --git a/stdlib/tst-concurrent-exit-skeleton.c b/stdlib/tst-concurrent-exit-skeleton.c
> new file mode 100644
> index 0000000000..cfd5140466
> --- /dev/null
> +++ b/stdlib/tst-concurrent-exit-skeleton.c
> @@ -0,0 +1,160 @@
> +/* Check if exit/quick_exit can be called concurrently by multiple threads.
> +   Copyright (C) 2024 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <array_length.h>
> +#include <stdlib.h>
> +#include <support/check.h>
> +#include <support/xthread.h>
> +#include <stdio.h>
> +#include <support/xunistd.h>
> +#include <string.h>
> +
> +/* A value larger than the minimum required by C/POSIX (32), to trigger a
> +   new block memory allocation.  */
> +#define MAX_atexit 64
> +
> +static pthread_barrier_t barrier;
> +
> +static void *
> +tf (void *closure)
> +{
> +  xpthread_barrier_wait (&barrier);
> +  EXIT (0);
> +
> +  return NULL;
> +}
> +
> +static const char expected[] = "00000000000000000000000000000000000"
> +			       "00000000000000000000003021121130211";
> +static char crumbs[sizeof (expected)];
> +static int next_slot = 0;
> +
> +static void
> +exit_with_flush (int code)
> +{
> +  fflush (stdout);
> +  /* glibc allows recursive EXIT, the ATEXIT handlers execution will be
> +     resumed from the where the previous EXIT was interrupted.  */
> +  EXIT (code);
> +}
> +
> +/* Take some time, so another thread potentially issue EXIT.  */
> +#define SETUP_NANOSLEEP \
> +  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
> +		 NULL) != 0)						\
> +    FAIL_EXIT1 ("nanosleep: %m")
> +
> +static void
> +fn0 (void)
> +{
> +  crumbs[next_slot++] = '0';
> +  SETUP_NANOSLEEP;
> +}
> +
> +static void
> +fn1 (void)
> +{
> +  crumbs[next_slot++] = '1';
> +  SETUP_NANOSLEEP;
> +}
> +
> +static void
> +fn2 (void)
> +{
> +  crumbs[next_slot++] = '2';
> +  ATEXIT (fn1);
> +  SETUP_NANOSLEEP;
> +}
> +
> +static void
> +fn3 (void)
> +{
> +  crumbs[next_slot++] = '3';
> +  ATEXIT (fn2);
> +  ATEXIT (fn0);
> +  SETUP_NANOSLEEP;
> +}
> +
> +static void
> +fn_final (void)
> +{
> +  TEST_COMPARE_STRING (crumbs, expected);
> +  exit_with_flush (0);
> +}
> +
> +_Noreturn static void
> +child (void)
> +{
> +  enum { nthreads = 8 };
> +
> +  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
> +
> +  pthread_t thr[nthreads];
> +  for (int i = 0; i < nthreads; i++)
> +    thr[i] = xpthread_create (NULL, tf, NULL);
> +
> +  xpthread_barrier_wait (&barrier);
> +
> +  for (int i = 0; i < nthreads; i++)
> +    {
> +      pthread_join (thr[i], NULL);
> +      /* It should not be reached, it means that thread did not exit for
> +	 some reason.  */
> +      support_record_failure ();
> +    }
> +
> +  EXIT (2);
> +}
> +
> +static int
> +do_test (void)
> +{
> +  /* Register a large number of handler that will trigger a heap allocation
> +     for the handle state.  On EXIT, each block will be freed after the
> +     handle is processed.  */
> +  int slots_remaining = MAX_atexit;
> +
> +  /* Register this first so it can verify expected order of the rest.  */
> +  ATEXIT (fn_final); --slots_remaining;
> +
> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
> +  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
> +  TEST_VERIFY_EXIT (ATEXIT (fn2) == 0); --slots_remaining;
> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
> +  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
> +
> +  while (slots_remaining > 0)
> +    {
> +      TEST_VERIFY_EXIT (ATEXIT (fn0) == 0); --slots_remaining;
> +    }

OK.

> +
> +  pid_t pid = xfork ();
> +  if (pid != 0)
> +    {
> +      int status;
> +      xwaitpid (pid, &status, 0);
> +      TEST_VERIFY (WIFEXITED (status));
> +    }
> +  else
> +    child ();
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/stdlib/tst-concurrent-exit.c b/stdlib/tst-concurrent-exit.c
> index 1141130f87..421c39d631 100644
> --- a/stdlib/tst-concurrent-exit.c
> +++ b/stdlib/tst-concurrent-exit.c
> @@ -16,142 +16,7 @@
>     License along with the GNU C Library; if not, see
>     <https://www.gnu.org/licenses/>.  */
>  
> -#include <array_length.h>
> -#include <stdlib.h>
> -#include <support/check.h>
> -#include <support/xthread.h>
> -#include <stdio.h>
> -#include <support/xunistd.h>
> -#include <string.h>
> +#define EXIT(__r)    exit (__r)
> +#define ATEXIT(__f)  atexit (__f)
>  
> -#define MAX_atexit 32
> -
> -static pthread_barrier_t barrier;
> -
> -static void *
> -tf (void *closure)
> -{
> -  xpthread_barrier_wait (&barrier);
> -  exit (0);
> -
> -  return NULL;
> -}
> -
> -static const char expected[] = "00000000000000000000000003021121130211";
> -static char crumbs[sizeof (expected)];
> -static int next_slot = 0;
> -
> -static void
> -exit_with_flush (int code)
> -{
> -  fflush (stdout);
> -  /* glibc allows recursive exit, the atexit handlers execution will be
> -     resumed from the where the previous exit was interrupted.  */
> -  exit (code);
> -}
> -
> -/* Take some time, so another thread potentially issue exit.  */
> -#define SETUP_NANOSLEEP \
> -  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
> -		 NULL) != 0)						\
> -    FAIL_EXIT1 ("nanosleep: %m")
> -
> -static void
> -fn0 (void)
> -{
> -  crumbs[next_slot++] = '0';
> -  SETUP_NANOSLEEP;
> -}
> -
> -static void
> -fn1 (void)
> -{
> -  crumbs[next_slot++] = '1';
> -  SETUP_NANOSLEEP;
> -}
> -
> -static void
> -fn2 (void)
> -{
> -  crumbs[next_slot++] = '2';
> -  atexit (fn1);
> -  SETUP_NANOSLEEP;
> -}
> -
> -static void
> -fn3 (void)
> -{
> -  crumbs[next_slot++] = '3';
> -  atexit (fn2);
> -  atexit (fn0);
> -  SETUP_NANOSLEEP;
> -}
> -
> -static void
> -fn_final (void)
> -{
> -  TEST_COMPARE_STRING (crumbs, expected);
> -  exit_with_flush (0);
> -}
> -
> -_Noreturn static void
> -child (void)
> -{
> -  enum { nthreads = 8 };
> -
> -  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
> -
> -  pthread_t thr[nthreads];
> -  for (int i = 0; i < nthreads; i++)
> -    thr[i] = xpthread_create (NULL, tf, NULL);
> -
> -  xpthread_barrier_wait (&barrier);
> -
> -  for (int i = 0; i < nthreads; i++)
> -    {
> -      pthread_join (thr[i], NULL);
> -      /* It should not be reached, it means that thread did not exit for
> -	 some reason.  */
> -      support_record_failure ();
> -    }
> -
> -  exit (2);
> -}
> -
> -static int
> -do_test (void)
> -{
> -  /* Register a large number of handler that will trigger a heap allocation
> -     for the handle state.  On exit, each block will be freed after the
> -     handle is processed.  */
> -  int slots_remaining = MAX_atexit;
> -
> -  /* Register this first so it can verify expected order of the rest.  */
> -  atexit (fn_final); --slots_remaining;
> -
> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
> -  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
> -  TEST_VERIFY_EXIT (atexit (fn2) == 0); --slots_remaining;
> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
> -  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
> -
> -  while (slots_remaining > 0)
> -    {
> -      TEST_VERIFY_EXIT (atexit (fn0) == 0); --slots_remaining;
> -    }
> -
> -  pid_t pid = xfork ();
> -  if (pid != 0)
> -    {
> -      int status;
> -      xwaitpid (pid, &status, 0);
> -      TEST_VERIFY (WIFEXITED (status));
> -    }
> -  else
> -    child ();
> -
> -  return 0;
> -}
> -
> -#include <support/test-driver.c>
> +#include "tst-concurrent-exit-skeleton.c"

OK. Cleanup.

> diff --git a/stdlib/tst-concurrent-quick_exit.c b/stdlib/tst-concurrent-quick_exit.c
> new file mode 100644
> index 0000000000..3f321668d6
> --- /dev/null
> +++ b/stdlib/tst-concurrent-quick_exit.c
> @@ -0,0 +1,22 @@
> +/* Check if quick_exit can be called concurrently by multiple threads.
> +   Copyright (C) 2024 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#define EXIT(__r)    quick_exit (__r)
> +#define ATEXIT(__f)  at_quick_exit (__f)
> +
> +#include "tst-concurrent-exit-skeleton.c"

OK.
Adhemerval Zanella Netto Aug. 5, 2024, 8:06 p.m. UTC | #2
On 05/08/24 13:06, Carlos O'Donell wrote:
> On 8/5/24 10:27 AM, Adhemerval Zanella wrote:
>> As for exit, also allows concurrent quick_exit to avoid race
>> conditions when it is called concurrently.  Since it uses the same
>> internal function as exit, the __exit_lock lock is moved to
>> __run_exit_handlers.  It also solved a potential concurrent when
>> calling exit and quick_exit concurrently.
> 
> Do we also have the same problem with dlclose() which is going to possibly remove
> an entire object along with registered functions?

We have in fact some issue with concurrent dlclose / exit:

  1. dclose might unmmap the object during exit __exit_func list transversal,
     and this might trigger invalid memory accesses [1].  I think this falls
     to the same issues of the under-specification of concurrent exit along
     other functions that access the same underlying data structure that holds
     the atexit/quick_exit/__cxa_atexit handlers.  

     This leads to different strategies on how to avoid such issues [2] (which
     afaik is not only presented on glibc, but on some other libc implementations).

     I think a cleanly solution would to if a dlopen shared object contains a
     binding to __cxa_finalize to mark if with DF_1_NODELETE.  This would change
     the expected semantic slight, but at least would avoid a whole set of
     subtle bugs.

  2. This seems to be also a small memory leak where __cxa_finalize does not
     free of the allocated chain (I just noted by reading the code, I need
     to check if it where the case indeed).

  3. Concurrent __cxa_finalize does not synchronize with exit/quick_exit. The 
     'removal' on __cxa_finalize is to avoid calling termination functions more 
     than once, and I think it is required.

[1] https://sourceware.org/bugzilla/show_bug.cgi?id=31285
[2] https://maskray.me/blog/2024-03-17-c++-exit-time-destructors

> 
> Should dlclose() take the same recursive lock such that a developer either sees
> the dlclose() happen first, followed by exit/quick_exit, or not see it at all?

There is a subtle issue where doing this would be tricky: _dl_fini, registered
at process startup (csu/libc-start.c:312) tries to lock the GL(dl_load_lock)
lock (elf/dl-fini.c:49).  And dlclose also takes the lock (elf/dl-close.c:763),
and thus trying to take the __exit_lock on __cxa_finalize could easily deadlock
the process. 

I still don't have a solution to this.


> 
>> The test case 'expected' is expanded to a value larger than the
>> minimum required by C/POSIX (32 entries) so at_quick_exit() will
>> require libc to allocate a new block.  This makes the test mre likely to
>> trigger concurrent issues (through free() at __run_exit_handlers)
>> if quick_exit() interacts with the at_quick_exit list concurrently.
>>
>> This is also the latest interpretation of the Austin Ticket [1].
> 
> LGTM. Though we may need a follow-up dlclose change.
> 
> Reviewed-by: Carlos O'Donell <carlos@redhat.com>

Thanks.

> 
>>
>> Checked on x86_64-linux-gnu.
>>
>> [1] https://austingroupbugs.net/view.php?id=1845
>> --
>> Changes from v1:
>> - Fixed lint-makefiles issue.
>> ---
>>  stdlib/Makefile                       |   1 +
>>  stdlib/exit.c                         |  18 +--
>>  stdlib/tst-concurrent-exit-skeleton.c | 160 ++++++++++++++++++++++++++
>>  stdlib/tst-concurrent-exit.c          | 141 +----------------------
>>  stdlib/tst-concurrent-quick_exit.c    |  22 ++++
>>  5 files changed, 196 insertions(+), 146 deletions(-)
>>  create mode 100644 stdlib/tst-concurrent-exit-skeleton.c
>>  create mode 100644 stdlib/tst-concurrent-quick_exit.c
>>
>> diff --git a/stdlib/Makefile b/stdlib/Makefile
>> index f659c38feb..043a452f72 100644
>> --- a/stdlib/Makefile
>> +++ b/stdlib/Makefile
>> @@ -274,6 +274,7 @@ tests := \
>>    tst-bz20544 \
>>    tst-canon-bz26341 \
>>    tst-concurrent-exit \
>> +  tst-concurrent-quick_exit \
> 
> OK.
> 
>>    tst-cxa_atexit \
>>    tst-environ \
>>    tst-getrandom \
>> diff --git a/stdlib/exit.c b/stdlib/exit.c
>> index bbaf138806..8d7e2e53d0 100644
>> --- a/stdlib/exit.c
>> +++ b/stdlib/exit.c
>> @@ -28,6 +28,13 @@
>>     __exit_funcs_lock is declared.  */
>>  bool __exit_funcs_done = false;
>>  
>> +/* The lock handles concurrent exit() and quick_exit(), even though the
>> +   C/POSIX standard states that calling exit() more than once is UB.  The
>> +   recursive lock allows atexit() handlers or destructors to call exit()
>> +   itself.  In this case, the  handler list execution will resume at the
>> +   point of the current handler.  */
>> +__libc_lock_define_initialized_recursive (static, __exit_lock)
> 
> OK. Moved up from below and added note about quick_exit.
> 
>> +
>>  /* Call all functions registered with `atexit' and `on_exit',
>>     in the reverse of the order in which they were registered
>>     perform stdio cleanup, and terminate program execution with STATUS.  */
>> @@ -36,6 +43,9 @@ attribute_hidden
>>  __run_exit_handlers (int status, struct exit_function_list **listp,
>>  		     bool run_list_atexit, bool run_dtors)
>>  {
>> +  /* The exit should never return, so there is no need to unlock it.  */
>> +  __libc_lock_lock_recursive (__exit_lock);
> 
> OK.
> 
>> +
>>    /* First, call the TLS destructors.  */
>>    if (run_dtors)
>>      call_function_static_weak (__call_tls_dtors);
>> @@ -132,17 +142,9 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
>>  }
>>  
>>  
>> -/* The lock handles concurrent exit(), even though the C/POSIX standard states
>> -   that calling exit() more than once is UB.  The recursive lock allows
>> -   atexit() handlers or destructors to call exit() itself.  In this case, the
>> -   handler list execution will resume at the point of the current handler.  */
>> -__libc_lock_define_initialized_recursive (static, __exit_lock)
>> -
> 
> OK.
> 
>>  void
>>  exit (int status)
>>  {
>> -  /* The exit should never return, so there is no need to unlock it.  */
>> -  __libc_lock_lock_recursive (__exit_lock);
> 
> OK. Moved into the handling of exit handlers.
> 
>>    __run_exit_handlers (status, &__exit_funcs, true, true);
>>  }
>>  libc_hidden_def (exit)
>> diff --git a/stdlib/tst-concurrent-exit-skeleton.c b/stdlib/tst-concurrent-exit-skeleton.c
>> new file mode 100644
>> index 0000000000..cfd5140466
>> --- /dev/null
>> +++ b/stdlib/tst-concurrent-exit-skeleton.c
>> @@ -0,0 +1,160 @@
>> +/* Check if exit/quick_exit can be called concurrently by multiple threads.
>> +   Copyright (C) 2024 Free Software Foundation, Inc.
>> +   This file is part of the GNU C Library.
>> +
>> +   The GNU C Library is free software; you can redistribute it and/or
>> +   modify it under the terms of the GNU Lesser General Public
>> +   License as published by the Free Software Foundation; either
>> +   version 2.1 of the License, or (at your option) any later version.
>> +
>> +   The GNU C Library is distributed in the hope that it will be useful,
>> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
>> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> +   Lesser General Public License for more details.
>> +
>> +   You should have received a copy of the GNU Lesser General Public
>> +   License along with the GNU C Library; if not, see
>> +   <https://www.gnu.org/licenses/>.  */
>> +
>> +#include <array_length.h>
>> +#include <stdlib.h>
>> +#include <support/check.h>
>> +#include <support/xthread.h>
>> +#include <stdio.h>
>> +#include <support/xunistd.h>
>> +#include <string.h>
>> +
>> +/* A value larger than the minimum required by C/POSIX (32), to trigger a
>> +   new block memory allocation.  */
>> +#define MAX_atexit 64
>> +
>> +static pthread_barrier_t barrier;
>> +
>> +static void *
>> +tf (void *closure)
>> +{
>> +  xpthread_barrier_wait (&barrier);
>> +  EXIT (0);
>> +
>> +  return NULL;
>> +}
>> +
>> +static const char expected[] = "00000000000000000000000000000000000"
>> +			       "00000000000000000000003021121130211";
>> +static char crumbs[sizeof (expected)];
>> +static int next_slot = 0;
>> +
>> +static void
>> +exit_with_flush (int code)
>> +{
>> +  fflush (stdout);
>> +  /* glibc allows recursive EXIT, the ATEXIT handlers execution will be
>> +     resumed from the where the previous EXIT was interrupted.  */
>> +  EXIT (code);
>> +}
>> +
>> +/* Take some time, so another thread potentially issue EXIT.  */
>> +#define SETUP_NANOSLEEP \
>> +  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
>> +		 NULL) != 0)						\
>> +    FAIL_EXIT1 ("nanosleep: %m")
>> +
>> +static void
>> +fn0 (void)
>> +{
>> +  crumbs[next_slot++] = '0';
>> +  SETUP_NANOSLEEP;
>> +}
>> +
>> +static void
>> +fn1 (void)
>> +{
>> +  crumbs[next_slot++] = '1';
>> +  SETUP_NANOSLEEP;
>> +}
>> +
>> +static void
>> +fn2 (void)
>> +{
>> +  crumbs[next_slot++] = '2';
>> +  ATEXIT (fn1);
>> +  SETUP_NANOSLEEP;
>> +}
>> +
>> +static void
>> +fn3 (void)
>> +{
>> +  crumbs[next_slot++] = '3';
>> +  ATEXIT (fn2);
>> +  ATEXIT (fn0);
>> +  SETUP_NANOSLEEP;
>> +}
>> +
>> +static void
>> +fn_final (void)
>> +{
>> +  TEST_COMPARE_STRING (crumbs, expected);
>> +  exit_with_flush (0);
>> +}
>> +
>> +_Noreturn static void
>> +child (void)
>> +{
>> +  enum { nthreads = 8 };
>> +
>> +  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
>> +
>> +  pthread_t thr[nthreads];
>> +  for (int i = 0; i < nthreads; i++)
>> +    thr[i] = xpthread_create (NULL, tf, NULL);
>> +
>> +  xpthread_barrier_wait (&barrier);
>> +
>> +  for (int i = 0; i < nthreads; i++)
>> +    {
>> +      pthread_join (thr[i], NULL);
>> +      /* It should not be reached, it means that thread did not exit for
>> +	 some reason.  */
>> +      support_record_failure ();
>> +    }
>> +
>> +  EXIT (2);
>> +}
>> +
>> +static int
>> +do_test (void)
>> +{
>> +  /* Register a large number of handler that will trigger a heap allocation
>> +     for the handle state.  On EXIT, each block will be freed after the
>> +     handle is processed.  */
>> +  int slots_remaining = MAX_atexit;
>> +
>> +  /* Register this first so it can verify expected order of the rest.  */
>> +  ATEXIT (fn_final); --slots_remaining;
>> +
>> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
>> +  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
>> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
>> +  TEST_VERIFY_EXIT (ATEXIT (fn2) == 0); --slots_remaining;
>> +  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
>> +  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
>> +
>> +  while (slots_remaining > 0)
>> +    {
>> +      TEST_VERIFY_EXIT (ATEXIT (fn0) == 0); --slots_remaining;
>> +    }
> 
> OK.
> 
>> +
>> +  pid_t pid = xfork ();
>> +  if (pid != 0)
>> +    {
>> +      int status;
>> +      xwaitpid (pid, &status, 0);
>> +      TEST_VERIFY (WIFEXITED (status));
>> +    }
>> +  else
>> +    child ();
>> +
>> +  return 0;
>> +}
>> +
>> +#include <support/test-driver.c>
>> diff --git a/stdlib/tst-concurrent-exit.c b/stdlib/tst-concurrent-exit.c
>> index 1141130f87..421c39d631 100644
>> --- a/stdlib/tst-concurrent-exit.c
>> +++ b/stdlib/tst-concurrent-exit.c
>> @@ -16,142 +16,7 @@
>>     License along with the GNU C Library; if not, see
>>     <https://www.gnu.org/licenses/>.  */
>>  
>> -#include <array_length.h>
>> -#include <stdlib.h>
>> -#include <support/check.h>
>> -#include <support/xthread.h>
>> -#include <stdio.h>
>> -#include <support/xunistd.h>
>> -#include <string.h>
>> +#define EXIT(__r)    exit (__r)
>> +#define ATEXIT(__f)  atexit (__f)
>>  
>> -#define MAX_atexit 32
>> -
>> -static pthread_barrier_t barrier;
>> -
>> -static void *
>> -tf (void *closure)
>> -{
>> -  xpthread_barrier_wait (&barrier);
>> -  exit (0);
>> -
>> -  return NULL;
>> -}
>> -
>> -static const char expected[] = "00000000000000000000000003021121130211";
>> -static char crumbs[sizeof (expected)];
>> -static int next_slot = 0;
>> -
>> -static void
>> -exit_with_flush (int code)
>> -{
>> -  fflush (stdout);
>> -  /* glibc allows recursive exit, the atexit handlers execution will be
>> -     resumed from the where the previous exit was interrupted.  */
>> -  exit (code);
>> -}
>> -
>> -/* Take some time, so another thread potentially issue exit.  */
>> -#define SETUP_NANOSLEEP \
>> -  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
>> -		 NULL) != 0)						\
>> -    FAIL_EXIT1 ("nanosleep: %m")
>> -
>> -static void
>> -fn0 (void)
>> -{
>> -  crumbs[next_slot++] = '0';
>> -  SETUP_NANOSLEEP;
>> -}
>> -
>> -static void
>> -fn1 (void)
>> -{
>> -  crumbs[next_slot++] = '1';
>> -  SETUP_NANOSLEEP;
>> -}
>> -
>> -static void
>> -fn2 (void)
>> -{
>> -  crumbs[next_slot++] = '2';
>> -  atexit (fn1);
>> -  SETUP_NANOSLEEP;
>> -}
>> -
>> -static void
>> -fn3 (void)
>> -{
>> -  crumbs[next_slot++] = '3';
>> -  atexit (fn2);
>> -  atexit (fn0);
>> -  SETUP_NANOSLEEP;
>> -}
>> -
>> -static void
>> -fn_final (void)
>> -{
>> -  TEST_COMPARE_STRING (crumbs, expected);
>> -  exit_with_flush (0);
>> -}
>> -
>> -_Noreturn static void
>> -child (void)
>> -{
>> -  enum { nthreads = 8 };
>> -
>> -  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
>> -
>> -  pthread_t thr[nthreads];
>> -  for (int i = 0; i < nthreads; i++)
>> -    thr[i] = xpthread_create (NULL, tf, NULL);
>> -
>> -  xpthread_barrier_wait (&barrier);
>> -
>> -  for (int i = 0; i < nthreads; i++)
>> -    {
>> -      pthread_join (thr[i], NULL);
>> -      /* It should not be reached, it means that thread did not exit for
>> -	 some reason.  */
>> -      support_record_failure ();
>> -    }
>> -
>> -  exit (2);
>> -}
>> -
>> -static int
>> -do_test (void)
>> -{
>> -  /* Register a large number of handler that will trigger a heap allocation
>> -     for the handle state.  On exit, each block will be freed after the
>> -     handle is processed.  */
>> -  int slots_remaining = MAX_atexit;
>> -
>> -  /* Register this first so it can verify expected order of the rest.  */
>> -  atexit (fn_final); --slots_remaining;
>> -
>> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
>> -  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
>> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
>> -  TEST_VERIFY_EXIT (atexit (fn2) == 0); --slots_remaining;
>> -  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
>> -  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
>> -
>> -  while (slots_remaining > 0)
>> -    {
>> -      TEST_VERIFY_EXIT (atexit (fn0) == 0); --slots_remaining;
>> -    }
>> -
>> -  pid_t pid = xfork ();
>> -  if (pid != 0)
>> -    {
>> -      int status;
>> -      xwaitpid (pid, &status, 0);
>> -      TEST_VERIFY (WIFEXITED (status));
>> -    }
>> -  else
>> -    child ();
>> -
>> -  return 0;
>> -}
>> -
>> -#include <support/test-driver.c>
>> +#include "tst-concurrent-exit-skeleton.c"
> 
> OK. Cleanup.
> 
>> diff --git a/stdlib/tst-concurrent-quick_exit.c b/stdlib/tst-concurrent-quick_exit.c
>> new file mode 100644
>> index 0000000000..3f321668d6
>> --- /dev/null
>> +++ b/stdlib/tst-concurrent-quick_exit.c
>> @@ -0,0 +1,22 @@
>> +/* Check if quick_exit can be called concurrently by multiple threads.
>> +   Copyright (C) 2024 Free Software Foundation, Inc.
>> +   This file is part of the GNU C Library.
>> +
>> +   The GNU C Library is free software; you can redistribute it and/or
>> +   modify it under the terms of the GNU Lesser General Public
>> +   License as published by the Free Software Foundation; either
>> +   version 2.1 of the License, or (at your option) any later version.
>> +
>> +   The GNU C Library is distributed in the hope that it will be useful,
>> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
>> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> +   Lesser General Public License for more details.
>> +
>> +   You should have received a copy of the GNU Lesser General Public
>> +   License along with the GNU C Library; if not, see
>> +   <https://www.gnu.org/licenses/>.  */
>> +
>> +#define EXIT(__r)    quick_exit (__r)
>> +#define ATEXIT(__f)  at_quick_exit (__f)
>> +
>> +#include "tst-concurrent-exit-skeleton.c"
> 
> OK.
>
Joseph Myers Aug. 6, 2024, 4:41 p.m. UTC | #3
The new test fails to link for Hurd (pthread_create not in libc).
diff mbox series

Patch

diff --git a/stdlib/Makefile b/stdlib/Makefile
index f659c38feb..043a452f72 100644
--- a/stdlib/Makefile
+++ b/stdlib/Makefile
@@ -274,6 +274,7 @@  tests := \
   tst-bz20544 \
   tst-canon-bz26341 \
   tst-concurrent-exit \
+  tst-concurrent-quick_exit \
   tst-cxa_atexit \
   tst-environ \
   tst-getrandom \
diff --git a/stdlib/exit.c b/stdlib/exit.c
index bbaf138806..8d7e2e53d0 100644
--- a/stdlib/exit.c
+++ b/stdlib/exit.c
@@ -28,6 +28,13 @@ 
    __exit_funcs_lock is declared.  */
 bool __exit_funcs_done = false;
 
+/* The lock handles concurrent exit() and quick_exit(), even though the
+   C/POSIX standard states that calling exit() more than once is UB.  The
+   recursive lock allows atexit() handlers or destructors to call exit()
+   itself.  In this case, the  handler list execution will resume at the
+   point of the current handler.  */
+__libc_lock_define_initialized_recursive (static, __exit_lock)
+
 /* Call all functions registered with `atexit' and `on_exit',
    in the reverse of the order in which they were registered
    perform stdio cleanup, and terminate program execution with STATUS.  */
@@ -36,6 +43,9 @@  attribute_hidden
 __run_exit_handlers (int status, struct exit_function_list **listp,
 		     bool run_list_atexit, bool run_dtors)
 {
+  /* The exit should never return, so there is no need to unlock it.  */
+  __libc_lock_lock_recursive (__exit_lock);
+
   /* First, call the TLS destructors.  */
   if (run_dtors)
     call_function_static_weak (__call_tls_dtors);
@@ -132,17 +142,9 @@  __run_exit_handlers (int status, struct exit_function_list **listp,
 }
 
 
-/* The lock handles concurrent exit(), even though the C/POSIX standard states
-   that calling exit() more than once is UB.  The recursive lock allows
-   atexit() handlers or destructors to call exit() itself.  In this case, the
-   handler list execution will resume at the point of the current handler.  */
-__libc_lock_define_initialized_recursive (static, __exit_lock)
-
 void
 exit (int status)
 {
-  /* The exit should never return, so there is no need to unlock it.  */
-  __libc_lock_lock_recursive (__exit_lock);
   __run_exit_handlers (status, &__exit_funcs, true, true);
 }
 libc_hidden_def (exit)
diff --git a/stdlib/tst-concurrent-exit-skeleton.c b/stdlib/tst-concurrent-exit-skeleton.c
new file mode 100644
index 0000000000..cfd5140466
--- /dev/null
+++ b/stdlib/tst-concurrent-exit-skeleton.c
@@ -0,0 +1,160 @@ 
+/* Check if exit/quick_exit can be called concurrently by multiple threads.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <array_length.h>
+#include <stdlib.h>
+#include <support/check.h>
+#include <support/xthread.h>
+#include <stdio.h>
+#include <support/xunistd.h>
+#include <string.h>
+
+/* A value larger than the minimum required by C/POSIX (32), to trigger a
+   new block memory allocation.  */
+#define MAX_atexit 64
+
+static pthread_barrier_t barrier;
+
+static void *
+tf (void *closure)
+{
+  xpthread_barrier_wait (&barrier);
+  EXIT (0);
+
+  return NULL;
+}
+
+static const char expected[] = "00000000000000000000000000000000000"
+			       "00000000000000000000003021121130211";
+static char crumbs[sizeof (expected)];
+static int next_slot = 0;
+
+static void
+exit_with_flush (int code)
+{
+  fflush (stdout);
+  /* glibc allows recursive EXIT, the ATEXIT handlers execution will be
+     resumed from the where the previous EXIT was interrupted.  */
+  EXIT (code);
+}
+
+/* Take some time, so another thread potentially issue EXIT.  */
+#define SETUP_NANOSLEEP \
+  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
+		 NULL) != 0)						\
+    FAIL_EXIT1 ("nanosleep: %m")
+
+static void
+fn0 (void)
+{
+  crumbs[next_slot++] = '0';
+  SETUP_NANOSLEEP;
+}
+
+static void
+fn1 (void)
+{
+  crumbs[next_slot++] = '1';
+  SETUP_NANOSLEEP;
+}
+
+static void
+fn2 (void)
+{
+  crumbs[next_slot++] = '2';
+  ATEXIT (fn1);
+  SETUP_NANOSLEEP;
+}
+
+static void
+fn3 (void)
+{
+  crumbs[next_slot++] = '3';
+  ATEXIT (fn2);
+  ATEXIT (fn0);
+  SETUP_NANOSLEEP;
+}
+
+static void
+fn_final (void)
+{
+  TEST_COMPARE_STRING (crumbs, expected);
+  exit_with_flush (0);
+}
+
+_Noreturn static void
+child (void)
+{
+  enum { nthreads = 8 };
+
+  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
+
+  pthread_t thr[nthreads];
+  for (int i = 0; i < nthreads; i++)
+    thr[i] = xpthread_create (NULL, tf, NULL);
+
+  xpthread_barrier_wait (&barrier);
+
+  for (int i = 0; i < nthreads; i++)
+    {
+      pthread_join (thr[i], NULL);
+      /* It should not be reached, it means that thread did not exit for
+	 some reason.  */
+      support_record_failure ();
+    }
+
+  EXIT (2);
+}
+
+static int
+do_test (void)
+{
+  /* Register a large number of handler that will trigger a heap allocation
+     for the handle state.  On EXIT, each block will be freed after the
+     handle is processed.  */
+  int slots_remaining = MAX_atexit;
+
+  /* Register this first so it can verify expected order of the rest.  */
+  ATEXIT (fn_final); --slots_remaining;
+
+  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
+  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
+  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
+  TEST_VERIFY_EXIT (ATEXIT (fn2) == 0); --slots_remaining;
+  TEST_VERIFY_EXIT (ATEXIT (fn1) == 0); --slots_remaining;
+  TEST_VERIFY_EXIT (ATEXIT (fn3) == 0); --slots_remaining;
+
+  while (slots_remaining > 0)
+    {
+      TEST_VERIFY_EXIT (ATEXIT (fn0) == 0); --slots_remaining;
+    }
+
+  pid_t pid = xfork ();
+  if (pid != 0)
+    {
+      int status;
+      xwaitpid (pid, &status, 0);
+      TEST_VERIFY (WIFEXITED (status));
+    }
+  else
+    child ();
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/stdlib/tst-concurrent-exit.c b/stdlib/tst-concurrent-exit.c
index 1141130f87..421c39d631 100644
--- a/stdlib/tst-concurrent-exit.c
+++ b/stdlib/tst-concurrent-exit.c
@@ -16,142 +16,7 @@ 
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
-#include <array_length.h>
-#include <stdlib.h>
-#include <support/check.h>
-#include <support/xthread.h>
-#include <stdio.h>
-#include <support/xunistd.h>
-#include <string.h>
+#define EXIT(__r)    exit (__r)
+#define ATEXIT(__f)  atexit (__f)
 
-#define MAX_atexit 32
-
-static pthread_barrier_t barrier;
-
-static void *
-tf (void *closure)
-{
-  xpthread_barrier_wait (&barrier);
-  exit (0);
-
-  return NULL;
-}
-
-static const char expected[] = "00000000000000000000000003021121130211";
-static char crumbs[sizeof (expected)];
-static int next_slot = 0;
-
-static void
-exit_with_flush (int code)
-{
-  fflush (stdout);
-  /* glibc allows recursive exit, the atexit handlers execution will be
-     resumed from the where the previous exit was interrupted.  */
-  exit (code);
-}
-
-/* Take some time, so another thread potentially issue exit.  */
-#define SETUP_NANOSLEEP \
-  if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L },	\
-		 NULL) != 0)						\
-    FAIL_EXIT1 ("nanosleep: %m")
-
-static void
-fn0 (void)
-{
-  crumbs[next_slot++] = '0';
-  SETUP_NANOSLEEP;
-}
-
-static void
-fn1 (void)
-{
-  crumbs[next_slot++] = '1';
-  SETUP_NANOSLEEP;
-}
-
-static void
-fn2 (void)
-{
-  crumbs[next_slot++] = '2';
-  atexit (fn1);
-  SETUP_NANOSLEEP;
-}
-
-static void
-fn3 (void)
-{
-  crumbs[next_slot++] = '3';
-  atexit (fn2);
-  atexit (fn0);
-  SETUP_NANOSLEEP;
-}
-
-static void
-fn_final (void)
-{
-  TEST_COMPARE_STRING (crumbs, expected);
-  exit_with_flush (0);
-}
-
-_Noreturn static void
-child (void)
-{
-  enum { nthreads = 8 };
-
-  xpthread_barrier_init (&barrier, NULL, nthreads + 1);
-
-  pthread_t thr[nthreads];
-  for (int i = 0; i < nthreads; i++)
-    thr[i] = xpthread_create (NULL, tf, NULL);
-
-  xpthread_barrier_wait (&barrier);
-
-  for (int i = 0; i < nthreads; i++)
-    {
-      pthread_join (thr[i], NULL);
-      /* It should not be reached, it means that thread did not exit for
-	 some reason.  */
-      support_record_failure ();
-    }
-
-  exit (2);
-}
-
-static int
-do_test (void)
-{
-  /* Register a large number of handler that will trigger a heap allocation
-     for the handle state.  On exit, each block will be freed after the
-     handle is processed.  */
-  int slots_remaining = MAX_atexit;
-
-  /* Register this first so it can verify expected order of the rest.  */
-  atexit (fn_final); --slots_remaining;
-
-  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
-  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
-  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
-  TEST_VERIFY_EXIT (atexit (fn2) == 0); --slots_remaining;
-  TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
-  TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
-
-  while (slots_remaining > 0)
-    {
-      TEST_VERIFY_EXIT (atexit (fn0) == 0); --slots_remaining;
-    }
-
-  pid_t pid = xfork ();
-  if (pid != 0)
-    {
-      int status;
-      xwaitpid (pid, &status, 0);
-      TEST_VERIFY (WIFEXITED (status));
-    }
-  else
-    child ();
-
-  return 0;
-}
-
-#include <support/test-driver.c>
+#include "tst-concurrent-exit-skeleton.c"
diff --git a/stdlib/tst-concurrent-quick_exit.c b/stdlib/tst-concurrent-quick_exit.c
new file mode 100644
index 0000000000..3f321668d6
--- /dev/null
+++ b/stdlib/tst-concurrent-quick_exit.c
@@ -0,0 +1,22 @@ 
+/* Check if quick_exit can be called concurrently by multiple threads.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#define EXIT(__r)    quick_exit (__r)
+#define ATEXIT(__f)  at_quick_exit (__f)
+
+#include "tst-concurrent-exit-skeleton.c"