posix: Avoid allocation while holding atfork_lock [BZ 34321]
Checks
| Context |
Check |
Description |
| redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
| redhat-pt-bot/TryBot-32bit |
success
|
Build for i686
|
| linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_glibc_build--master-arm |
success
|
Build passed
|
Commit Message
__register_atfork inserted the new handler into a dynamic array while
holding atfork_lock, and growing that array calls the public/iterposable
malloc. An allocator replacement whose own lock is also taken from a
registered pthread_atfork prepare handler can therefore deadlock:
Thread A: fork Thread B: __register_atfork
------------------------------------- -----------------------------------
:139 unlock(atfork_lock)
:141 prepare() -> lock(L) [holds L]
:39 lock(atfork_lock) [holds atfork]
:44 emplace -> malloc -> lock(L)
BLOCKS on L (held by A)
:144 lock(atfork_lock)
BLOCKS on atfork (held by B)
=> DEADLOCK (A: holds L, waits atfork; B: holds atfork, waits L)
Replace the dynamic array with a doubly linked list (include/list.h).
A new node is allocated outside atfork_lock and only linked in while the
lock is held. __unregister_atfork moves nodes to a free pool instead of
freeing them, and __register_atfork reuses a pooled node when one is
available, so neither path allocates or frees under atfork_lock in the
steady state; malloc runs only to grow the pool, always outside the lock.
This preserves the current allocation guarantee of the dynarray.
Checked on x86_64-linux-gnu and aarch64-linux-gnu.
---
include/register-atfork.h | 5 +-
posix/register-atfork.c | 278 ++++++++++++++++++++--------------
sysdeps/pthread/Makefile | 3 +
sysdeps/pthread/tst-atfork5.c | 227 +++++++++++++++++++++++++++
4 files changed, 397 insertions(+), 116 deletions(-)
create mode 100644 sysdeps/pthread/tst-atfork5.c
Comments
On Sat, Jun 27, 2026 at 1:31 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> __register_atfork inserted the new handler into a dynamic array while
> holding atfork_lock, and growing that array calls the public/iterposable
> malloc. An allocator replacement whose own lock is also taken from a
> registered pthread_atfork prepare handler can therefore deadlock:
>
> Thread A: fork Thread B: __register_atfork
> ------------------------------------- -----------------------------------
> :139 unlock(atfork_lock)
> :141 prepare() -> lock(L) [holds L]
> :39 lock(atfork_lock) [holds atfork]
> :44 emplace -> malloc -> lock(L)
> BLOCKS on L (held by A)
> :144 lock(atfork_lock)
> BLOCKS on atfork (held by B)
> => DEADLOCK (A: holds L, waits atfork; B: holds atfork, waits L)
>
> Replace the dynamic array with a doubly linked list (include/list.h).
> A new node is allocated outside atfork_lock and only linked in while the
> lock is held. __unregister_atfork moves nodes to a free pool instead of
> freeing them, and __register_atfork reuses a pooled node when one is
> available, so neither path allocates or frees under atfork_lock in the
> steady state; malloc runs only to grow the pool, always outside the lock.
> This preserves the current allocation guarantee of the dynarray.
>
> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
Is it possible to create a run-time test with an alternate malloc via
LD_PRELOAD on systems where alternate malloc is available
similar to
https://gitlab.com/x86-glibc/glibc/-/commits/users/hjl/test/malloc
> ---
> include/register-atfork.h | 5 +-
> posix/register-atfork.c | 278 ++++++++++++++++++++--------------
> sysdeps/pthread/Makefile | 3 +
> sysdeps/pthread/tst-atfork5.c | 227 +++++++++++++++++++++++++++
> 4 files changed, 397 insertions(+), 116 deletions(-)
> create mode 100644 sysdeps/pthread/tst-atfork5.c
>
> diff --git a/include/register-atfork.h b/include/register-atfork.h
> index 25286a25aad..2fc0064d9d8 100644
> --- a/include/register-atfork.h
> +++ b/include/register-atfork.h
> @@ -19,7 +19,9 @@
> #ifndef _REGISTER_ATFORK_H
> #define _REGISTER_ATFORK_H
>
> -/* Elements of the fork handler lists. */
> +#include <list_t.h>
> +
> +/* Elements of the fork handler list. */
> struct fork_handler
> {
> void (*prepare_handler) (void);
> @@ -27,6 +29,7 @@ struct fork_handler
> void (*child_handler) (void);
> void *dso_handle;
> uint64_t id;
> + list_t list;
> };
>
> /* Function to call to unregister fork handlers. */
> diff --git a/posix/register-atfork.c b/posix/register-atfork.c
> index 580c6b77caf..ef20f5cbf7e 100644
> --- a/posix/register-atfork.c
> +++ b/posix/register-atfork.c
> @@ -17,94 +17,121 @@
>
> #include <libc-lock.h>
> #include <stdbool.h>
> +#include <stdlib.h>
> +#include <errno.h>
> #include <register-atfork.h>
> #include <intprops.h>
> +#include <list.h>
> #include <stdio.h>
>
> -#define DYNARRAY_ELEMENT struct fork_handler
> -#define DYNARRAY_STRUCT fork_handler_list
> -#define DYNARRAY_PREFIX fork_handler_list_
> -#define DYNARRAY_INITIAL_SIZE 48
> -#include <malloc/dynarray-skeleton.c>
> +/* New handlers are added at the front, so the list runs from the newest
> + (highest ID) at the front to the oldest (lowest ID) at the back. A new
> + node is allocated with malloc only when no reusable node is available, and
> + always *outside* of ATFORK_LOCK: holding ATFORK_LOCK across an allocation
> + would deadlock with interposable allocators whose own locks are also taken
> + from an atfork handler. */
> +static LIST_HEAD (fork_handlers);
> +
> +/* Unregistered nodes are moved here (under ATFORK_LOCK) instead of being
> + freed, and a subsequent registration reuses one. This keeps both
> + registration and unregistration allocation-free in the steady state (in
> + particular avoids any free under ATFORK_LOCK). The pool is released only
> + by __libc_atfork_freemem. */
> +static LIST_HEAD (fork_handlers_free);
>
> -static struct fork_handler_list fork_handlers;
> static uint64_t fork_handler_counter;
>
> +/* Bumped on every list mutation (registration or unregistration). The fork
> + handler runners drop ATFORK_LOCK while a handler executes; comparing this
> + counter across that window tells them whether the list changed and a saved
> + list position is therefore still valid. */
> +static uint64_t fork_handler_modcount;
> +
> static int atfork_lock = LLL_LOCK_INITIALIZER;
>
> +#define fork_handler_entry(pos) list_entry (pos, struct fork_handler, list)
> +
> +/* True if the list LP has no elements. */
> +static inline bool
> +list_is_empty (list_t *lp)
> +{
> + return lp->next == lp;
> +}
> +
> +/* Initialize NEWP from the supplied handlers and link it at the front of the
> + active list. ATFORK_LOCK must be held. */
> +static void
> +fork_handler_init (struct fork_handler *newp, void (*prepare) (void),
> + void (*parent) (void), void (*child) (void),
> + void *dso_handle)
> +{
> + newp->prepare_handler = prepare;
> + newp->parent_handler = parent;
> + newp->child_handler = child;
> + newp->dso_handle = dso_handle;
> +
> + /* IDs assigned to handlers start at 1 and increment with handler
> + registration. Un-registering a handler discards the corresponding ID.
> + It is not reused in future registrations. */
> + if (INT_ADD_OVERFLOW (fork_handler_counter, 1))
> + __libc_fatal ("fork handler counter overflow");
> + newp->id = ++fork_handler_counter;
> +
> + /* Add at the front: the highest ID ends up first. */
> + list_add (&newp->list, &fork_handlers);
> +
> + ++fork_handler_modcount;
> +}
> +
> int
> __register_atfork (void (*prepare) (void), void (*parent) (void),
> void (*child) (void), void *dso_handle)
> {
> lll_lock (atfork_lock, LLL_PRIVATE);
>
> - if (fork_handler_counter == 0)
> - fork_handler_list_init (&fork_handlers);
> -
> - struct fork_handler *newp = fork_handler_list_emplace (&fork_handlers);
> - if (newp != NULL)
> + bool free_empty = list_is_empty (&fork_handlers_free);
> + if (!free_empty)
> {
> - newp->prepare_handler = prepare;
> - newp->parent_handler = parent;
> - newp->child_handler = child;
> - newp->dso_handle = dso_handle;
> -
> - /* IDs assigned to handlers start at 1 and increment with handler
> - registration. Un-registering a handlers discards the corresponding
> - ID. It is not reused in future registrations. */
> - if (INT_ADD_OVERFLOW (fork_handler_counter, 1))
> - __libc_fatal ("fork handler counter overflow");
> - newp->id = ++fork_handler_counter;
> + list_t *reuse = fork_handlers_free.next;
> + list_del (reuse);
> + fork_handler_init (fork_handler_entry (reuse), prepare, parent, child,
> + dso_handle);
> }
>
> - /* Release the lock. */
> lll_unlock (atfork_lock, LLL_PRIVATE);
>
> - return newp == NULL ? ENOMEM : 0;
> + if (!free_empty)
> + return 0;
> +
> + struct fork_handler *newp = malloc (sizeof (*newp));
> + if (newp == NULL)
> + return ENOMEM;
> +
> + lll_lock (atfork_lock, LLL_PRIVATE);
> + fork_handler_init (newp, prepare, parent, child, dso_handle);
> + lll_unlock (atfork_lock, LLL_PRIVATE);
> +
> + return 0;
> }
> libc_hidden_def (__register_atfork)
>
> -static struct fork_handler *
> -fork_handler_list_find (struct fork_handler_list *fork_handlers,
> - void *dso_handle)
> -{
> - for (size_t i = 0; i < fork_handler_list_size (fork_handlers); i++)
> - {
> - struct fork_handler *elem = fork_handler_list_at (fork_handlers, i);
> - if (elem->dso_handle == dso_handle)
> - return elem;
> - }
> - return NULL;
> -}
> -
> void
> __unregister_atfork (void *dso_handle)
> {
> + list_t *runp, *prevp;
> +
> lll_lock (atfork_lock, LLL_PRIVATE);
>
> - struct fork_handler *first = fork_handler_list_find (&fork_handlers,
> - dso_handle);
> - /* Removing is done by shifting the elements in the way the elements
> - that are not to be removed appear in the beginning in dynarray.
> - This avoid the quadradic run-time if a naive strategy to remove and
> - shift one element at time. */
> - if (first != NULL)
> - {
> - struct fork_handler *new_end = first;
> - first++;
> - for (; first != fork_handler_list_end (&fork_handlers); ++first)
> - {
> - if (first->dso_handle != dso_handle)
> - {
> - *new_end = *first;
> - ++new_end;
> - }
> - }
> -
> - ptrdiff_t removed = first - new_end;
> - for (size_t i = 0; i < removed; i++)
> - fork_handler_list_remove_last (&fork_handlers);
> - }
> + /* Move the matching nodes to the free pool rather than freeing them: this
> + avoids a free under ATFORK_LOCK and lets a later registration reuse
> + them. */
> + list_for_each_prev_safe (runp, prevp, &fork_handlers)
> + if (fork_handler_entry (runp)->dso_handle == dso_handle)
> + {
> + list_del (runp);
> + list_add (runp, &fork_handlers_free);
> + ++fork_handler_modcount;
> + }
>
> lll_unlock (atfork_lock, LLL_PRIVATE);
> }
> @@ -117,47 +144,55 @@ __run_prefork_handlers (_Bool do_locking)
> if (do_locking)
> lll_lock (atfork_lock, LLL_PRIVATE);
>
> - /* We run prepare handlers from last to first. After fork, only
> - handlers up to the last handler found here (pre-fork) will be run.
> - Handlers registered during __run_prefork_handlers or
> - __run_postfork_handlers will be positioned after this last handler, and
> - since their prepare handlers won't be run now, their parent/child
> - handlers should also be ignored. */
> + /* We run prepare handlers from newest to oldest (highest ID first). After
> + fork, only handlers up to the last handler found here (pre-fork) will be
> + run. Handlers registered during __run_prefork_handlers or
> + __run_postfork_handlers will have a higher ID, and since their prepare
> + handlers will not be run now, their parent/child handlers should also be
> + ignored. */
> lastrun = fork_handler_counter;
>
> - size_t sl = fork_handler_list_size (&fork_handlers);
> - for (size_t i = sl; i > 0;)
> + /* The newest handlers are at the front; skip any with ID > LASTRUN (those
> + were registered after this function was entered). */
> + list_t *pos = fork_handlers.next;
> + while (pos != &fork_handlers && fork_handler_entry (pos)->id > lastrun)
> + pos = pos->next;
> +
> + while (pos != &fork_handlers)
> {
> - struct fork_handler *runp
> - = fork_handler_list_at (&fork_handlers, i - 1);
> -
> + struct fork_handler *runp = fork_handler_entry (pos);
> uint64_t id = runp->id;
> + void (*prepare_handler) (void) = runp->prepare_handler;
>
> - if (runp->prepare_handler != NULL)
> + /* Remember where to continue and whether the list changed across the
> + handler call. Both are read while the lock is held. */
> + uint64_t saved_modcount = fork_handler_modcount;
> + list_t *next = pos->next;
> +
> + if (prepare_handler != NULL)
> {
> if (do_locking)
> lll_unlock (atfork_lock, LLL_PRIVATE);
>
> - runp->prepare_handler ();
> + prepare_handler ();
>
> if (do_locking)
> lll_lock (atfork_lock, LLL_PRIVATE);
> }
>
> - /* We unlocked, ran the handler, and locked again. In the
> - meanwhile, one or more deregistrations could have occurred leading
> - to the current (just run) handler being moved up the list or even
> - removed from the list itself. Since handler IDs are guaranteed to
> - to be in increasing order, the next handler has to have: */
> -
> - /* A. An earlier position than the current one has. */
> - i--;
> -
> - /* B. A lower ID than the current one does. The code below skips
> - any newly added handlers with higher IDs. */
> - while (i > 0
> - && fork_handler_list_at (&fork_handlers, i - 1)->id >= id)
> - i--;
> + /* Advance to the next (lower ID) handler. If nothing was registered or
> + unregistered while the lock was dropped, the saved position is still
> + valid. Otherwise it may be stale (a node could even have been
> + freed), so re-find the next handler by ID: the one with the greatest
> + ID strictly below the just-run handler. */
> + if (fork_handler_modcount == saved_modcount)
> + pos = next;
> + else
> + {
> + pos = fork_handlers.next;
> + while (pos != &fork_handlers && fork_handler_entry (pos)->id >= id)
> + pos = pos->next;
> + }
> }
>
> return lastrun;
> @@ -167,10 +202,12 @@ void
> __run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
> uint64_t lastrun)
> {
> - size_t sl = fork_handler_list_size (&fork_handlers);
> - for (size_t i = 0; i < sl;)
> + /* Run parent/child handlers from oldest to newest (lowest ID first). The
> + oldest handlers are at the back of the list. */
> + list_t *pos = fork_handlers.prev;
> + while (pos != &fork_handlers)
> {
> - struct fork_handler *runp = fork_handler_list_at (&fork_handlers, i);
> + struct fork_handler *runp = fork_handler_entry (pos);
> uint64_t id = runp->id;
>
> /* prepare handlers were not run for handlers with ID > LASTRUN.
> @@ -178,37 +215,36 @@ __run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
> if (id > lastrun)
> break;
>
> - if (do_locking)
> - lll_unlock (atfork_lock, LLL_PRIVATE);
> + void (*handler) (void) = NULL;
> + if (who == atfork_run_child)
> + handler = runp->child_handler;
> + else if (who == atfork_run_parent)
> + handler = runp->parent_handler;
>
> - if (who == atfork_run_child && runp->child_handler)
> - runp->child_handler ();
> - else if (who == atfork_run_parent && runp->parent_handler)
> - runp->parent_handler ();
> + uint64_t saved_modcount = fork_handler_modcount;
> + list_t *prev = pos->prev;
>
> - if (do_locking)
> - lll_lock (atfork_lock, LLL_PRIVATE);
> + if (handler != NULL)
> + {
> + if (do_locking)
> + lll_unlock (atfork_lock, LLL_PRIVATE);
>
> - /* We unlocked, ran the handler, and locked again. In the meanwhile,
> - one or more [de]registrations could have occurred. Due to this,
> - the list size must be updated. */
> - sl = fork_handler_list_size (&fork_handlers);
> + handler ();
>
> - /* The just-run handler could also have moved up the list. */
> + if (do_locking)
> + lll_lock (atfork_lock, LLL_PRIVATE);
> + }
>
> - if (sl > i && fork_handler_list_at (&fork_handlers, i)->id == id)
> - /* The position of the recently run handler hasn't changed. The
> - next handler to be run is an easy increment away. */
> - i++;
> + /* Advance to the next (higher ID) handler. Fast path when the list did
> + not change; otherwise re-find by ID the handler with the smallest ID
> + strictly above the just-run one. */
> + if (fork_handler_modcount == saved_modcount)
> + pos = prev;
> else
> {
> - /* The next handler to be run is the first handler in the list
> - to have an ID higher than the current one. */
> - for (i = 0; i < sl; i++)
> - {
> - if (fork_handler_list_at (&fork_handlers, i)->id > id)
> - break;
> - }
> + pos = fork_handlers.prev;
> + while (pos != &fork_handlers && fork_handler_entry (pos)->id <= id)
> + pos = pos->prev;
> }
> }
>
> @@ -220,9 +256,21 @@ __run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
> void
> __libc_atfork_freemem (void)
> {
> + /* Detach both the active list and the free pool under the lock and free
> + them afterwards. */
> + LIST_HEAD (removed);
> + list_t *runp, *prevp;
> +
> lll_lock (atfork_lock, LLL_PRIVATE);
>
> - fork_handler_list_free (&fork_handlers);
> + list_splice (&fork_handlers, &removed);
> + INIT_LIST_HEAD (&fork_handlers);
> +
> + list_splice (&fork_handlers_free, &removed);
> + INIT_LIST_HEAD (&fork_handlers_free);
>
> lll_unlock (atfork_lock, LLL_PRIVATE);
> +
> + list_for_each_prev_safe (runp, prevp, &removed)
> + free (fork_handler_entry (runp));
> }
> diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile
> index d0f3cd59ac6..5718a262c33 100644
> --- a/sysdeps/pthread/Makefile
> +++ b/sysdeps/pthread/Makefile
> @@ -58,6 +58,7 @@ routines += \
> tests += \
> tst-abstime \
> tst-atfork1 \
> + tst-atfork5 \
> tst-attr1 \
> tst-bad-schedattr \
> tst-barrier1 \
> @@ -459,6 +460,8 @@ $(objpfx)tst-atfork4: $(shared-thread-library)
> LDFLAGS-tst-atfork4 = -rdynamic
> $(objpfx)tst-atfork4mod.so: $(shared-thread-library)
>
> +$(objpfx)tst-atfork5: $(shared-thread-library)
> +
> ifeq ($(build-shared),yes)
> $(objpfx)tst-atfork2.out: $(objpfx)tst-atfork2mod.so
> $(objpfx)tst-atfork3.out: $(objpfx)tst-atfork3mod.so
> diff --git a/sysdeps/pthread/tst-atfork5.c b/sysdeps/pthread/tst-atfork5.c
> new file mode 100644
> index 00000000000..c247ac97945
> --- /dev/null
> +++ b/sysdeps/pthread/tst-atfork5.c
> @@ -0,0 +1,227 @@
> +/* Test that atfork registration does not deadlock against a malloc
> + replacement (BZ 34321).
> + Copyright (C) 2026 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/>. */
> +
> +/* Check if a malloc replacement that takes its own lock both in malloc and
> + in a registered fork prepare handler does not deadlock. One thread holds
> + the atfork lock and waits for the allocator lock inside __register_atfork,
> + while the forking thread holds the allocator lock in the prepare handler
> + and waits for the atfork lock. */
> +
> +#include <dlfcn.h>
> +#include <pthread.h>
> +#include <stdatomic.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <sys/wait.h>
> +
> +#include <support/check.h>
> +#include <support/xthread.h>
> +#include <support/xunistd.h>
> +#include <support/xdlfcn.h>
> +
> +#define WINDOW_US 4000 /* useconds_t prepare handler holds ARENA_LOCK. */
> +#define NREG 3 /* Number of 'tf' threads. */
> +#define FORK_TARGET 20 /* Forks to complete when there is no deadlock. */
> +#define REG_CAP 40000U /* Upper bound on total handler registrations. */
> +
> +/* Minimal malloc interposer: every allocation takes ARENA_LOCK, and so does
> + the fork prepare handler below. */
> +static pthread_mutex_t arena_lock = PTHREAD_MUTEX_INITIALIZER;
> +static void *(*real_malloc) (size_t);
> +static void *(*real_calloc) (size_t, size_t);
> +static void *(*real_realloc) (void *, size_t);
> +static void (*real_free) (void *);
> +
> +/* Small bump buffer ti provide the few allocations that happen while dlsym
> + itself runs. */
> +static __thread bool in_dlsym;
> +static char bootbuf[1 << 20];
> +static size_t bootoff;
> +
> +static int
> +is_boot (void *p)
> +{
> + return (char *) p >= bootbuf && (char *) p < bootbuf + sizeof bootbuf;
> +}
> +
> +static void *
> +boot_alloc (size_t n)
> +{
> + size_t o = (bootoff + 15) & ~(size_t) 15;
> + bootoff = o + n;
> + return bootbuf + o;
> +}
> +
> +static void
> +init_real (void)
> +{
> + if (real_malloc != NULL)
> + return;
> + in_dlsym = true;
> + real_malloc = xdlsym (RTLD_NEXT, "malloc");
> + real_calloc = xdlsym (RTLD_NEXT, "calloc");
> + real_realloc = xdlsym (RTLD_NEXT, "realloc");
> + real_free = xdlsym (RTLD_NEXT, "free");
> + in_dlsym = false;
> +}
> +
> +void *
> +malloc (size_t n)
> +{
> + if (real_malloc == NULL)
> + {
> + if (in_dlsym)
> + return boot_alloc (n);
> + init_real ();
> + }
> + pthread_mutex_lock (&arena_lock);
> + void *p = real_malloc (n);
> + pthread_mutex_unlock (&arena_lock);
> + return p;
> +}
> +
> +void *
> +calloc (size_t a, size_t b)
> +{
> + if (real_malloc == NULL)
> + {
> + if (in_dlsym)
> + return boot_alloc (a * b);
> + init_real ();
> + }
> + pthread_mutex_lock (&arena_lock);
> + void *p = real_calloc (a, b);
> + pthread_mutex_unlock (&arena_lock);
> + return p;
> +}
> +
> +void *
> +realloc (void *old, size_t n)
> +{
> + if (real_malloc == NULL)
> + init_real ();
> + pthread_mutex_lock (&arena_lock);
> + void *p = real_realloc (old, n);
> + pthread_mutex_unlock (&arena_lock);
> + return p;
> +}
> +
> +void
> +free (void *p)
> +{
> + if (is_boot (p))
> + return;
> + if (real_free == NULL)
> + init_real ();
> + xpthread_mutex_lock (&arena_lock);
> + real_free (p);
> + xpthread_mutex_unlock (&arena_lock);
> +}
> +
> +/* Synchronizes the NREG tf threads with the first prepare handler run, so
> + that they only start registering once ARENA_LOCK is held by the prepare
> + handler below. */
> +static pthread_barrier_t window_barrier;
> +static atomic_int synced;
> +
> +static void
> +prepare (void)
> +{
> + xpthread_mutex_lock (&arena_lock);
> + /* Release the tf threads on the first fork only; on later forks they are
> + no longer waiting on the barrier. */
> + if (!atomic_exchange (&synced, 1))
> + xpthread_barrier_wait (&window_barrier);
> + usleep (WINDOW_US);
> +}
> +
> +static void
> +parent (void)
> +{
> + xpthread_mutex_unlock (&arena_lock);
> +}
> +
> +static void
> +child (void)
> +{
> + xpthread_mutex_unlock (&arena_lock);
> +}
> +
> +__attribute__ ((constructor))
> +static void
> +init (void)
> +{
> + init_real ();
> +}
> +
> +static atomic_int running = 1;
> +static atomic_int forks_done;
> +static atomic_uint reg_count;
> +
> +static void *
> +tf (void *closure)
> +{
> + /* Start registering only once a prepare window is open (ARENA_LOCK held),
> + so the first handler-array growth happens under that lock. The total
> + number of registrations is bounded so that the handler list stays small
> + and, on a fixed library, the fork handlers run quickly. */
> + xpthread_barrier_wait (&window_barrier);
> + while (atomic_load (&running)
> + && atomic_fetch_add (®_count, 1) < REG_CAP)
> + pthread_atfork (NULL, NULL, NULL);
> + return NULL;
> +}
> +
> +static void *
> +forker (void *closure)
> +{
> + while (atomic_load (&running))
> + {
> + pid_t pid = xfork ();
> + if (pid == 0)
> + _exit (0);
> + xwaitpid (pid, NULL, 0);
> + if (atomic_fetch_add (&forks_done, 1) + 1 >= FORK_TARGET)
> + break;
> + }
> + return NULL;
> +}
> +
> +static int
> +do_test (void)
> +{
> + xpthread_barrier_init (&window_barrier, NULL, NREG + 1);
> + TEST_COMPARE (pthread_atfork (prepare, parent, child), 0);
> +
> + pthread_t reg[NREG];
> + for (int i = 0; i < NREG; i++)
> + reg[i] = xpthread_create (NULL, tf, NULL);
> + pthread_t fork_tid = xpthread_create (NULL, forker, NULL);
> +
> + xpthread_join (fork_tid);
> + atomic_store (&running, 0);
> + for (int i = 0; i < NREG; i++)
> + xpthread_join (reg[i]);
> +
> + xpthread_barrier_destroy (&window_barrier);
> + return 0;
> +}
> +
> +#define TIMEOUT 8
> +#include <support/test-driver.c>
> --
> 2.43.0
>
On 26/06/26 19:59, H.J. Lu wrote:
> On Sat, Jun 27, 2026 at 1:31 AM Adhemerval Zanella
> <adhemerval.zanella@linaro.org> wrote:
>>
>> __register_atfork inserted the new handler into a dynamic array while
>> holding atfork_lock, and growing that array calls the public/iterposable
>> malloc. An allocator replacement whose own lock is also taken from a
>> registered pthread_atfork prepare handler can therefore deadlock:
>>
>> Thread A: fork Thread B: __register_atfork
>> ------------------------------------- -----------------------------------
>> :139 unlock(atfork_lock)
>> :141 prepare() -> lock(L) [holds L]
>> :39 lock(atfork_lock) [holds atfork]
>> :44 emplace -> malloc -> lock(L)
>> BLOCKS on L (held by A)
>> :144 lock(atfork_lock)
>> BLOCKS on atfork (held by B)
>> => DEADLOCK (A: holds L, waits atfork; B: holds atfork, waits L)
>>
>> Replace the dynamic array with a doubly linked list (include/list.h).
>> A new node is allocated outside atfork_lock and only linked in while the
>> lock is held. __unregister_atfork moves nodes to a free pool instead of
>> freeing them, and __register_atfork reuses a pooled node when one is
>> available, so neither path allocates or frees under atfork_lock in the
>> steady state; malloc runs only to grow the pool, always outside the lock.
>> This preserves the current allocation guarantee of the dynarray.
>>
>> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
>
> Is it possible to create a run-time test with an alternate malloc via
> LD_PRELOAD on systems where alternate malloc is available
> similar to
>
> https://gitlab.com/x86-glibc/glibc/-/commits/users/hjl/test/malloc
I think for checking this issue being a LD_PRELOAD or malloc implemented
in the binary itself does not really matter. The mechanism glibc will used
to trigger is the same, and being on the binary makes it simpler.
On Mon, Jun 29, 2026 at 7:14 PM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
>
>
>
> On 26/06/26 19:59, H.J. Lu wrote:
> > On Sat, Jun 27, 2026 at 1:31 AM Adhemerval Zanella
> > <adhemerval.zanella@linaro.org> wrote:
> >>
> >> __register_atfork inserted the new handler into a dynamic array while
> >> holding atfork_lock, and growing that array calls the public/iterposable
> >> malloc. An allocator replacement whose own lock is also taken from a
> >> registered pthread_atfork prepare handler can therefore deadlock:
> >>
> >> Thread A: fork Thread B: __register_atfork
> >> ------------------------------------- -----------------------------------
> >> :139 unlock(atfork_lock)
> >> :141 prepare() -> lock(L) [holds L]
> >> :39 lock(atfork_lock) [holds atfork]
> >> :44 emplace -> malloc -> lock(L)
> >> BLOCKS on L (held by A)
> >> :144 lock(atfork_lock)
> >> BLOCKS on atfork (held by B)
> >> => DEADLOCK (A: holds L, waits atfork; B: holds atfork, waits L)
> >>
> >> Replace the dynamic array with a doubly linked list (include/list.h).
> >> A new node is allocated outside atfork_lock and only linked in while the
> >> lock is held. __unregister_atfork moves nodes to a free pool instead of
> >> freeing them, and __register_atfork reuses a pooled node when one is
> >> available, so neither path allocates or frees under atfork_lock in the
> >> steady state; malloc runs only to grow the pool, always outside the lock.
> >> This preserves the current allocation guarantee of the dynarray.
> >>
> >> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
> >
> > Is it possible to create a run-time test with an alternate malloc via
> > LD_PRELOAD on systems where alternate malloc is available
> > similar to
> >
> > https://gitlab.com/x86-glibc/glibc/-/commits/users/hjl/test/malloc
> I think for checking this issue being a LD_PRELOAD or malloc implemented
> in the binary itself does not really matter. The mechanism glibc will used
> to trigger is the same, and being on the binary makes it simpler.
Without LD_PRELOAD, we can't be sure that the issue is really fixed.
We may have fixed a slighly different issue.
On 29/06/26 18:55, H.J. Lu wrote:
> On Mon, Jun 29, 2026 at 7:14 PM Adhemerval Zanella Netto
> <adhemerval.zanella@linaro.org> wrote:
>>
>>
>>
>> On 26/06/26 19:59, H.J. Lu wrote:
>>> On Sat, Jun 27, 2026 at 1:31 AM Adhemerval Zanella
>>> <adhemerval.zanella@linaro.org> wrote:
>>>>
>>>> __register_atfork inserted the new handler into a dynamic array while
>>>> holding atfork_lock, and growing that array calls the public/iterposable
>>>> malloc. An allocator replacement whose own lock is also taken from a
>>>> registered pthread_atfork prepare handler can therefore deadlock:
>>>>
>>>> Thread A: fork Thread B: __register_atfork
>>>> ------------------------------------- -----------------------------------
>>>> :139 unlock(atfork_lock)
>>>> :141 prepare() -> lock(L) [holds L]
>>>> :39 lock(atfork_lock) [holds atfork]
>>>> :44 emplace -> malloc -> lock(L)
>>>> BLOCKS on L (held by A)
>>>> :144 lock(atfork_lock)
>>>> BLOCKS on atfork (held by B)
>>>> => DEADLOCK (A: holds L, waits atfork; B: holds atfork, waits L)
>>>>
>>>> Replace the dynamic array with a doubly linked list (include/list.h).
>>>> A new node is allocated outside atfork_lock and only linked in while the
>>>> lock is held. __unregister_atfork moves nodes to a free pool instead of
>>>> freeing them, and __register_atfork reuses a pooled node when one is
>>>> available, so neither path allocates or frees under atfork_lock in the
>>>> steady state; malloc runs only to grow the pool, always outside the lock.
>>>> This preserves the current allocation guarantee of the dynarray.
>>>>
>>>> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
>>>
>>> Is it possible to create a run-time test with an alternate malloc via
>>> LD_PRELOAD on systems where alternate malloc is available
>>> similar to
>>>
>>> https://gitlab.com/x86-glibc/glibc/-/commits/users/hjl/test/malloc
>> I think for checking this issue being a LD_PRELOAD or malloc implemented
>> in the binary itself does not really matter. The mechanism glibc will used
>> to trigger is the same, and being on the binary makes it simpler.
>
> Without LD_PRELOAD, we can't be sure that the issue is really fixed.
> We may have fixed a slighly different issue.
I don't think this applies for the bug report, since what needs to be tested
is whether the process is using malloc interposition and if a pre-atfork
lock does not trigger a deadlock when multiple atfork handler are being
registered. It does not really matter if the malloc implementation is being
provided by LD_PRELOAD or by the binary itself, the underlying mechanism if
the same.
I can adapt to use LD_PRELOAD, it is just more boilerplate code though.
@@ -19,7 +19,9 @@
#ifndef _REGISTER_ATFORK_H
#define _REGISTER_ATFORK_H
-/* Elements of the fork handler lists. */
+#include <list_t.h>
+
+/* Elements of the fork handler list. */
struct fork_handler
{
void (*prepare_handler) (void);
@@ -27,6 +29,7 @@ struct fork_handler
void (*child_handler) (void);
void *dso_handle;
uint64_t id;
+ list_t list;
};
/* Function to call to unregister fork handlers. */
@@ -17,94 +17,121 @@
#include <libc-lock.h>
#include <stdbool.h>
+#include <stdlib.h>
+#include <errno.h>
#include <register-atfork.h>
#include <intprops.h>
+#include <list.h>
#include <stdio.h>
-#define DYNARRAY_ELEMENT struct fork_handler
-#define DYNARRAY_STRUCT fork_handler_list
-#define DYNARRAY_PREFIX fork_handler_list_
-#define DYNARRAY_INITIAL_SIZE 48
-#include <malloc/dynarray-skeleton.c>
+/* New handlers are added at the front, so the list runs from the newest
+ (highest ID) at the front to the oldest (lowest ID) at the back. A new
+ node is allocated with malloc only when no reusable node is available, and
+ always *outside* of ATFORK_LOCK: holding ATFORK_LOCK across an allocation
+ would deadlock with interposable allocators whose own locks are also taken
+ from an atfork handler. */
+static LIST_HEAD (fork_handlers);
+
+/* Unregistered nodes are moved here (under ATFORK_LOCK) instead of being
+ freed, and a subsequent registration reuses one. This keeps both
+ registration and unregistration allocation-free in the steady state (in
+ particular avoids any free under ATFORK_LOCK). The pool is released only
+ by __libc_atfork_freemem. */
+static LIST_HEAD (fork_handlers_free);
-static struct fork_handler_list fork_handlers;
static uint64_t fork_handler_counter;
+/* Bumped on every list mutation (registration or unregistration). The fork
+ handler runners drop ATFORK_LOCK while a handler executes; comparing this
+ counter across that window tells them whether the list changed and a saved
+ list position is therefore still valid. */
+static uint64_t fork_handler_modcount;
+
static int atfork_lock = LLL_LOCK_INITIALIZER;
+#define fork_handler_entry(pos) list_entry (pos, struct fork_handler, list)
+
+/* True if the list LP has no elements. */
+static inline bool
+list_is_empty (list_t *lp)
+{
+ return lp->next == lp;
+}
+
+/* Initialize NEWP from the supplied handlers and link it at the front of the
+ active list. ATFORK_LOCK must be held. */
+static void
+fork_handler_init (struct fork_handler *newp, void (*prepare) (void),
+ void (*parent) (void), void (*child) (void),
+ void *dso_handle)
+{
+ newp->prepare_handler = prepare;
+ newp->parent_handler = parent;
+ newp->child_handler = child;
+ newp->dso_handle = dso_handle;
+
+ /* IDs assigned to handlers start at 1 and increment with handler
+ registration. Un-registering a handler discards the corresponding ID.
+ It is not reused in future registrations. */
+ if (INT_ADD_OVERFLOW (fork_handler_counter, 1))
+ __libc_fatal ("fork handler counter overflow");
+ newp->id = ++fork_handler_counter;
+
+ /* Add at the front: the highest ID ends up first. */
+ list_add (&newp->list, &fork_handlers);
+
+ ++fork_handler_modcount;
+}
+
int
__register_atfork (void (*prepare) (void), void (*parent) (void),
void (*child) (void), void *dso_handle)
{
lll_lock (atfork_lock, LLL_PRIVATE);
- if (fork_handler_counter == 0)
- fork_handler_list_init (&fork_handlers);
-
- struct fork_handler *newp = fork_handler_list_emplace (&fork_handlers);
- if (newp != NULL)
+ bool free_empty = list_is_empty (&fork_handlers_free);
+ if (!free_empty)
{
- newp->prepare_handler = prepare;
- newp->parent_handler = parent;
- newp->child_handler = child;
- newp->dso_handle = dso_handle;
-
- /* IDs assigned to handlers start at 1 and increment with handler
- registration. Un-registering a handlers discards the corresponding
- ID. It is not reused in future registrations. */
- if (INT_ADD_OVERFLOW (fork_handler_counter, 1))
- __libc_fatal ("fork handler counter overflow");
- newp->id = ++fork_handler_counter;
+ list_t *reuse = fork_handlers_free.next;
+ list_del (reuse);
+ fork_handler_init (fork_handler_entry (reuse), prepare, parent, child,
+ dso_handle);
}
- /* Release the lock. */
lll_unlock (atfork_lock, LLL_PRIVATE);
- return newp == NULL ? ENOMEM : 0;
+ if (!free_empty)
+ return 0;
+
+ struct fork_handler *newp = malloc (sizeof (*newp));
+ if (newp == NULL)
+ return ENOMEM;
+
+ lll_lock (atfork_lock, LLL_PRIVATE);
+ fork_handler_init (newp, prepare, parent, child, dso_handle);
+ lll_unlock (atfork_lock, LLL_PRIVATE);
+
+ return 0;
}
libc_hidden_def (__register_atfork)
-static struct fork_handler *
-fork_handler_list_find (struct fork_handler_list *fork_handlers,
- void *dso_handle)
-{
- for (size_t i = 0; i < fork_handler_list_size (fork_handlers); i++)
- {
- struct fork_handler *elem = fork_handler_list_at (fork_handlers, i);
- if (elem->dso_handle == dso_handle)
- return elem;
- }
- return NULL;
-}
-
void
__unregister_atfork (void *dso_handle)
{
+ list_t *runp, *prevp;
+
lll_lock (atfork_lock, LLL_PRIVATE);
- struct fork_handler *first = fork_handler_list_find (&fork_handlers,
- dso_handle);
- /* Removing is done by shifting the elements in the way the elements
- that are not to be removed appear in the beginning in dynarray.
- This avoid the quadradic run-time if a naive strategy to remove and
- shift one element at time. */
- if (first != NULL)
- {
- struct fork_handler *new_end = first;
- first++;
- for (; first != fork_handler_list_end (&fork_handlers); ++first)
- {
- if (first->dso_handle != dso_handle)
- {
- *new_end = *first;
- ++new_end;
- }
- }
-
- ptrdiff_t removed = first - new_end;
- for (size_t i = 0; i < removed; i++)
- fork_handler_list_remove_last (&fork_handlers);
- }
+ /* Move the matching nodes to the free pool rather than freeing them: this
+ avoids a free under ATFORK_LOCK and lets a later registration reuse
+ them. */
+ list_for_each_prev_safe (runp, prevp, &fork_handlers)
+ if (fork_handler_entry (runp)->dso_handle == dso_handle)
+ {
+ list_del (runp);
+ list_add (runp, &fork_handlers_free);
+ ++fork_handler_modcount;
+ }
lll_unlock (atfork_lock, LLL_PRIVATE);
}
@@ -117,47 +144,55 @@ __run_prefork_handlers (_Bool do_locking)
if (do_locking)
lll_lock (atfork_lock, LLL_PRIVATE);
- /* We run prepare handlers from last to first. After fork, only
- handlers up to the last handler found here (pre-fork) will be run.
- Handlers registered during __run_prefork_handlers or
- __run_postfork_handlers will be positioned after this last handler, and
- since their prepare handlers won't be run now, their parent/child
- handlers should also be ignored. */
+ /* We run prepare handlers from newest to oldest (highest ID first). After
+ fork, only handlers up to the last handler found here (pre-fork) will be
+ run. Handlers registered during __run_prefork_handlers or
+ __run_postfork_handlers will have a higher ID, and since their prepare
+ handlers will not be run now, their parent/child handlers should also be
+ ignored. */
lastrun = fork_handler_counter;
- size_t sl = fork_handler_list_size (&fork_handlers);
- for (size_t i = sl; i > 0;)
+ /* The newest handlers are at the front; skip any with ID > LASTRUN (those
+ were registered after this function was entered). */
+ list_t *pos = fork_handlers.next;
+ while (pos != &fork_handlers && fork_handler_entry (pos)->id > lastrun)
+ pos = pos->next;
+
+ while (pos != &fork_handlers)
{
- struct fork_handler *runp
- = fork_handler_list_at (&fork_handlers, i - 1);
-
+ struct fork_handler *runp = fork_handler_entry (pos);
uint64_t id = runp->id;
+ void (*prepare_handler) (void) = runp->prepare_handler;
- if (runp->prepare_handler != NULL)
+ /* Remember where to continue and whether the list changed across the
+ handler call. Both are read while the lock is held. */
+ uint64_t saved_modcount = fork_handler_modcount;
+ list_t *next = pos->next;
+
+ if (prepare_handler != NULL)
{
if (do_locking)
lll_unlock (atfork_lock, LLL_PRIVATE);
- runp->prepare_handler ();
+ prepare_handler ();
if (do_locking)
lll_lock (atfork_lock, LLL_PRIVATE);
}
- /* We unlocked, ran the handler, and locked again. In the
- meanwhile, one or more deregistrations could have occurred leading
- to the current (just run) handler being moved up the list or even
- removed from the list itself. Since handler IDs are guaranteed to
- to be in increasing order, the next handler has to have: */
-
- /* A. An earlier position than the current one has. */
- i--;
-
- /* B. A lower ID than the current one does. The code below skips
- any newly added handlers with higher IDs. */
- while (i > 0
- && fork_handler_list_at (&fork_handlers, i - 1)->id >= id)
- i--;
+ /* Advance to the next (lower ID) handler. If nothing was registered or
+ unregistered while the lock was dropped, the saved position is still
+ valid. Otherwise it may be stale (a node could even have been
+ freed), so re-find the next handler by ID: the one with the greatest
+ ID strictly below the just-run handler. */
+ if (fork_handler_modcount == saved_modcount)
+ pos = next;
+ else
+ {
+ pos = fork_handlers.next;
+ while (pos != &fork_handlers && fork_handler_entry (pos)->id >= id)
+ pos = pos->next;
+ }
}
return lastrun;
@@ -167,10 +202,12 @@ void
__run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
uint64_t lastrun)
{
- size_t sl = fork_handler_list_size (&fork_handlers);
- for (size_t i = 0; i < sl;)
+ /* Run parent/child handlers from oldest to newest (lowest ID first). The
+ oldest handlers are at the back of the list. */
+ list_t *pos = fork_handlers.prev;
+ while (pos != &fork_handlers)
{
- struct fork_handler *runp = fork_handler_list_at (&fork_handlers, i);
+ struct fork_handler *runp = fork_handler_entry (pos);
uint64_t id = runp->id;
/* prepare handlers were not run for handlers with ID > LASTRUN.
@@ -178,37 +215,36 @@ __run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
if (id > lastrun)
break;
- if (do_locking)
- lll_unlock (atfork_lock, LLL_PRIVATE);
+ void (*handler) (void) = NULL;
+ if (who == atfork_run_child)
+ handler = runp->child_handler;
+ else if (who == atfork_run_parent)
+ handler = runp->parent_handler;
- if (who == atfork_run_child && runp->child_handler)
- runp->child_handler ();
- else if (who == atfork_run_parent && runp->parent_handler)
- runp->parent_handler ();
+ uint64_t saved_modcount = fork_handler_modcount;
+ list_t *prev = pos->prev;
- if (do_locking)
- lll_lock (atfork_lock, LLL_PRIVATE);
+ if (handler != NULL)
+ {
+ if (do_locking)
+ lll_unlock (atfork_lock, LLL_PRIVATE);
- /* We unlocked, ran the handler, and locked again. In the meanwhile,
- one or more [de]registrations could have occurred. Due to this,
- the list size must be updated. */
- sl = fork_handler_list_size (&fork_handlers);
+ handler ();
- /* The just-run handler could also have moved up the list. */
+ if (do_locking)
+ lll_lock (atfork_lock, LLL_PRIVATE);
+ }
- if (sl > i && fork_handler_list_at (&fork_handlers, i)->id == id)
- /* The position of the recently run handler hasn't changed. The
- next handler to be run is an easy increment away. */
- i++;
+ /* Advance to the next (higher ID) handler. Fast path when the list did
+ not change; otherwise re-find by ID the handler with the smallest ID
+ strictly above the just-run one. */
+ if (fork_handler_modcount == saved_modcount)
+ pos = prev;
else
{
- /* The next handler to be run is the first handler in the list
- to have an ID higher than the current one. */
- for (i = 0; i < sl; i++)
- {
- if (fork_handler_list_at (&fork_handlers, i)->id > id)
- break;
- }
+ pos = fork_handlers.prev;
+ while (pos != &fork_handlers && fork_handler_entry (pos)->id <= id)
+ pos = pos->prev;
}
}
@@ -220,9 +256,21 @@ __run_postfork_handlers (enum __run_fork_handler_type who, _Bool do_locking,
void
__libc_atfork_freemem (void)
{
+ /* Detach both the active list and the free pool under the lock and free
+ them afterwards. */
+ LIST_HEAD (removed);
+ list_t *runp, *prevp;
+
lll_lock (atfork_lock, LLL_PRIVATE);
- fork_handler_list_free (&fork_handlers);
+ list_splice (&fork_handlers, &removed);
+ INIT_LIST_HEAD (&fork_handlers);
+
+ list_splice (&fork_handlers_free, &removed);
+ INIT_LIST_HEAD (&fork_handlers_free);
lll_unlock (atfork_lock, LLL_PRIVATE);
+
+ list_for_each_prev_safe (runp, prevp, &removed)
+ free (fork_handler_entry (runp));
}
@@ -58,6 +58,7 @@ routines += \
tests += \
tst-abstime \
tst-atfork1 \
+ tst-atfork5 \
tst-attr1 \
tst-bad-schedattr \
tst-barrier1 \
@@ -459,6 +460,8 @@ $(objpfx)tst-atfork4: $(shared-thread-library)
LDFLAGS-tst-atfork4 = -rdynamic
$(objpfx)tst-atfork4mod.so: $(shared-thread-library)
+$(objpfx)tst-atfork5: $(shared-thread-library)
+
ifeq ($(build-shared),yes)
$(objpfx)tst-atfork2.out: $(objpfx)tst-atfork2mod.so
$(objpfx)tst-atfork3.out: $(objpfx)tst-atfork3mod.so
new file mode 100644
@@ -0,0 +1,227 @@
+/* Test that atfork registration does not deadlock against a malloc
+ replacement (BZ 34321).
+ Copyright (C) 2026 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/>. */
+
+/* Check if a malloc replacement that takes its own lock both in malloc and
+ in a registered fork prepare handler does not deadlock. One thread holds
+ the atfork lock and waits for the allocator lock inside __register_atfork,
+ while the forking thread holds the allocator lock in the prepare handler
+ and waits for the atfork lock. */
+
+#include <dlfcn.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include <support/check.h>
+#include <support/xthread.h>
+#include <support/xunistd.h>
+#include <support/xdlfcn.h>
+
+#define WINDOW_US 4000 /* useconds_t prepare handler holds ARENA_LOCK. */
+#define NREG 3 /* Number of 'tf' threads. */
+#define FORK_TARGET 20 /* Forks to complete when there is no deadlock. */
+#define REG_CAP 40000U /* Upper bound on total handler registrations. */
+
+/* Minimal malloc interposer: every allocation takes ARENA_LOCK, and so does
+ the fork prepare handler below. */
+static pthread_mutex_t arena_lock = PTHREAD_MUTEX_INITIALIZER;
+static void *(*real_malloc) (size_t);
+static void *(*real_calloc) (size_t, size_t);
+static void *(*real_realloc) (void *, size_t);
+static void (*real_free) (void *);
+
+/* Small bump buffer ti provide the few allocations that happen while dlsym
+ itself runs. */
+static __thread bool in_dlsym;
+static char bootbuf[1 << 20];
+static size_t bootoff;
+
+static int
+is_boot (void *p)
+{
+ return (char *) p >= bootbuf && (char *) p < bootbuf + sizeof bootbuf;
+}
+
+static void *
+boot_alloc (size_t n)
+{
+ size_t o = (bootoff + 15) & ~(size_t) 15;
+ bootoff = o + n;
+ return bootbuf + o;
+}
+
+static void
+init_real (void)
+{
+ if (real_malloc != NULL)
+ return;
+ in_dlsym = true;
+ real_malloc = xdlsym (RTLD_NEXT, "malloc");
+ real_calloc = xdlsym (RTLD_NEXT, "calloc");
+ real_realloc = xdlsym (RTLD_NEXT, "realloc");
+ real_free = xdlsym (RTLD_NEXT, "free");
+ in_dlsym = false;
+}
+
+void *
+malloc (size_t n)
+{
+ if (real_malloc == NULL)
+ {
+ if (in_dlsym)
+ return boot_alloc (n);
+ init_real ();
+ }
+ pthread_mutex_lock (&arena_lock);
+ void *p = real_malloc (n);
+ pthread_mutex_unlock (&arena_lock);
+ return p;
+}
+
+void *
+calloc (size_t a, size_t b)
+{
+ if (real_malloc == NULL)
+ {
+ if (in_dlsym)
+ return boot_alloc (a * b);
+ init_real ();
+ }
+ pthread_mutex_lock (&arena_lock);
+ void *p = real_calloc (a, b);
+ pthread_mutex_unlock (&arena_lock);
+ return p;
+}
+
+void *
+realloc (void *old, size_t n)
+{
+ if (real_malloc == NULL)
+ init_real ();
+ pthread_mutex_lock (&arena_lock);
+ void *p = real_realloc (old, n);
+ pthread_mutex_unlock (&arena_lock);
+ return p;
+}
+
+void
+free (void *p)
+{
+ if (is_boot (p))
+ return;
+ if (real_free == NULL)
+ init_real ();
+ xpthread_mutex_lock (&arena_lock);
+ real_free (p);
+ xpthread_mutex_unlock (&arena_lock);
+}
+
+/* Synchronizes the NREG tf threads with the first prepare handler run, so
+ that they only start registering once ARENA_LOCK is held by the prepare
+ handler below. */
+static pthread_barrier_t window_barrier;
+static atomic_int synced;
+
+static void
+prepare (void)
+{
+ xpthread_mutex_lock (&arena_lock);
+ /* Release the tf threads on the first fork only; on later forks they are
+ no longer waiting on the barrier. */
+ if (!atomic_exchange (&synced, 1))
+ xpthread_barrier_wait (&window_barrier);
+ usleep (WINDOW_US);
+}
+
+static void
+parent (void)
+{
+ xpthread_mutex_unlock (&arena_lock);
+}
+
+static void
+child (void)
+{
+ xpthread_mutex_unlock (&arena_lock);
+}
+
+__attribute__ ((constructor))
+static void
+init (void)
+{
+ init_real ();
+}
+
+static atomic_int running = 1;
+static atomic_int forks_done;
+static atomic_uint reg_count;
+
+static void *
+tf (void *closure)
+{
+ /* Start registering only once a prepare window is open (ARENA_LOCK held),
+ so the first handler-array growth happens under that lock. The total
+ number of registrations is bounded so that the handler list stays small
+ and, on a fixed library, the fork handlers run quickly. */
+ xpthread_barrier_wait (&window_barrier);
+ while (atomic_load (&running)
+ && atomic_fetch_add (®_count, 1) < REG_CAP)
+ pthread_atfork (NULL, NULL, NULL);
+ return NULL;
+}
+
+static void *
+forker (void *closure)
+{
+ while (atomic_load (&running))
+ {
+ pid_t pid = xfork ();
+ if (pid == 0)
+ _exit (0);
+ xwaitpid (pid, NULL, 0);
+ if (atomic_fetch_add (&forks_done, 1) + 1 >= FORK_TARGET)
+ break;
+ }
+ return NULL;
+}
+
+static int
+do_test (void)
+{
+ xpthread_barrier_init (&window_barrier, NULL, NREG + 1);
+ TEST_COMPARE (pthread_atfork (prepare, parent, child), 0);
+
+ pthread_t reg[NREG];
+ for (int i = 0; i < NREG; i++)
+ reg[i] = xpthread_create (NULL, tf, NULL);
+ pthread_t fork_tid = xpthread_create (NULL, forker, NULL);
+
+ xpthread_join (fork_tid);
+ atomic_store (&running, 0);
+ for (int i = 0; i < NREG; i++)
+ xpthread_join (reg[i]);
+
+ xpthread_barrier_destroy (&window_barrier);
+ return 0;
+}
+
+#define TIMEOUT 8
+#include <support/test-driver.c>