Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit b5d7318

Browse files
zatrazzchiichen
authored andcommitted
stdlib: Allow concurrent exit (BZ 31997)
Even if C/POSIX standard states that exit is not formally thread-unsafe, calling it more than once is UB. The glibc already supports it for the single-thread, and both elf/nodelete2.c and tst-rseq-disable.c call exit from a DSO destructor (which is called by _dl_fini, registered at program startup with __cxa_atexit). However, there are still race issues when it is called more than once concurrently by multiple threads. A recent Rust PR triggered this issue [1], which resulted in an Austin Group ask for clarification [2]. Besides it, there is a discussion to make concurrent calling not UB [3], wtih a defined semantic where any remaining callers block until the first call to exit has finished (reentrant calls, leaving through longjmp, and exceptions are still undefined). For glibc, at least reentrant calls are required to be supported to avoid changing the current behaviour. This requires locking using a recursive lock, where any exit called by atexit() handlers resumes at the point of the current handler (thus avoiding calling the current handle multiple times). Checked on x86_64-linux-gnu and aarch64-linux-gnu. [1] rust-lang/rust#126600 [2] https://austingroupbugs.net/view.php?id=1845 [3] https://www.openwall.com/lists/libc-coord/2024/07/24/4 Reviewed-by: Carlos O'Donell <[email protected]>
1 parent d040b88 commit b5d7318

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed

stdlib/Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ tests := \
273273
tst-bsearch \
274274
tst-bz20544 \
275275
tst-canon-bz26341 \
276+
tst-concurrent-exit \
276277
tst-cxa_atexit \
277278
tst-environ \
278279
tst-getrandom \

stdlib/exit.c

+8
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,17 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
132132
}
133133

134134

135+
/* The lock handles concurrent exit(), even though the C/POSIX standard states
136+
that calling exit() more than once is UB. The recursive lock allows
137+
atexit() handlers or destructors to call exit() itself. In this case, the
138+
handler list execution will resume at the point of the current handler. */
139+
__libc_lock_define_initialized_recursive (static, __exit_lock)
140+
135141
void
136142
exit (int status)
137143
{
144+
/* The exit should never return, so there is no need to unlock it. */
145+
__libc_lock_lock_recursive (__exit_lock);
138146
__run_exit_handlers (status, &__exit_funcs, true, true);
139147
}
140148
libc_hidden_def (exit)

stdlib/tst-concurrent-exit.c

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* Check if exit can be called concurrently by multiple threads.
2+
Copyright (C) 2024 Free Software Foundation, Inc.
3+
This file is part of the GNU C Library.
4+
5+
The GNU C Library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
The GNU C Library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with the GNU C Library; if not, see
17+
<https://www.gnu.org/licenses/>. */
18+
19+
#include <array_length.h>
20+
#include <stdlib.h>
21+
#include <support/check.h>
22+
#include <support/xthread.h>
23+
#include <stdio.h>
24+
#include <support/xunistd.h>
25+
#include <string.h>
26+
27+
#define MAX_atexit 32
28+
29+
static pthread_barrier_t barrier;
30+
31+
static void *
32+
tf (void *closure)
33+
{
34+
xpthread_barrier_wait (&barrier);
35+
exit (0);
36+
37+
return NULL;
38+
}
39+
40+
static const char expected[] = "00000000000000000000000003021121130211";
41+
static char crumbs[sizeof (expected)];
42+
static int next_slot = 0;
43+
44+
static void
45+
exit_with_flush (int code)
46+
{
47+
fflush (stdout);
48+
/* glibc allows recursive exit, the atexit handlers execution will be
49+
resumed from the where the previous exit was interrupted. */
50+
exit (code);
51+
}
52+
53+
/* Take some time, so another thread potentially issue exit. */
54+
#define SETUP_NANOSLEEP \
55+
if (nanosleep (&(struct timespec) { .tv_sec = 0, .tv_nsec = 1000L }, \
56+
NULL) != 0) \
57+
FAIL_EXIT1 ("nanosleep: %m")
58+
59+
static void
60+
fn0 (void)
61+
{
62+
crumbs[next_slot++] = '0';
63+
SETUP_NANOSLEEP;
64+
}
65+
66+
static void
67+
fn1 (void)
68+
{
69+
crumbs[next_slot++] = '1';
70+
SETUP_NANOSLEEP;
71+
}
72+
73+
static void
74+
fn2 (void)
75+
{
76+
crumbs[next_slot++] = '2';
77+
atexit (fn1);
78+
SETUP_NANOSLEEP;
79+
}
80+
81+
static void
82+
fn3 (void)
83+
{
84+
crumbs[next_slot++] = '3';
85+
atexit (fn2);
86+
atexit (fn0);
87+
SETUP_NANOSLEEP;
88+
}
89+
90+
static void
91+
fn_final (void)
92+
{
93+
TEST_COMPARE_STRING (crumbs, expected);
94+
exit_with_flush (0);
95+
}
96+
97+
_Noreturn static void
98+
child (void)
99+
{
100+
enum { nthreads = 8 };
101+
102+
xpthread_barrier_init (&barrier, NULL, nthreads + 1);
103+
104+
pthread_t thr[nthreads];
105+
for (int i = 0; i < nthreads; i++)
106+
thr[i] = xpthread_create (NULL, tf, NULL);
107+
108+
xpthread_barrier_wait (&barrier);
109+
110+
for (int i = 0; i < nthreads; i++)
111+
{
112+
pthread_join (thr[i], NULL);
113+
/* It should not be reached, it means that thread did not exit for
114+
some reason. */
115+
support_record_failure ();
116+
}
117+
118+
exit (2);
119+
}
120+
121+
static int
122+
do_test (void)
123+
{
124+
/* Register a large number of handler that will trigger a heap allocation
125+
for the handle state. On exit, each block will be freed after the
126+
handle is processed. */
127+
int slots_remaining = MAX_atexit;
128+
129+
/* Register this first so it can verify expected order of the rest. */
130+
atexit (fn_final); --slots_remaining;
131+
132+
TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
133+
TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
134+
TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
135+
TEST_VERIFY_EXIT (atexit (fn2) == 0); --slots_remaining;
136+
TEST_VERIFY_EXIT (atexit (fn1) == 0); --slots_remaining;
137+
TEST_VERIFY_EXIT (atexit (fn3) == 0); --slots_remaining;
138+
139+
while (slots_remaining > 0)
140+
{
141+
TEST_VERIFY_EXIT (atexit (fn0) == 0); --slots_remaining;
142+
}
143+
144+
pid_t pid = xfork ();
145+
if (pid != 0)
146+
{
147+
int status;
148+
xwaitpid (pid, &status, 0);
149+
TEST_VERIFY (WIFEXITED (status));
150+
}
151+
else
152+
child ();
153+
154+
return 0;
155+
}
156+
157+
#include <support/test-driver.c>

0 commit comments

Comments
 (0)