[PATCH v2] posix: Avoid allocation while holding atfork_lock [BZ 34321]

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Tue Sep 15 11:22:24 GMT 2026



On 11/09/26 21:20, H.J. Lu wrote:
> On Fri, Aug 14, 2026 at 11:43 PM 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.
> 
> Does this fix any reported jemalloc bugs like
> 
> https://github.com/jemalloc/jemalloc/issues/1805

I don't think so because this is an unrelated issue. This seems to be a
jemalloc-internal fork-unsafety, where the rtree rtree_node_init is a bare
atomic flag not covered by jemalloc_prefork() (and this is documented on
src/jemalloc.c:3289).

Also, the reporter's platform (Amazon Linux 1, glibc 2.17) can't exhibit
BZ 34321 at all. The __register_atfork only started calling malloc under
atfork_lock with the dynarray refactor 27761a1042d (glibc 2.28), before
that, nptl used an mmap-based handler pool precisely to avoid malloc there.

> 
>> Checked on x86_64-linux-gnu and aarch64-linux-gnu.
>> ---
>>  include/register-atfork.h        |   5 +-
>>  posix/register-atfork.c          | 278 ++++++++++++++++++-------------
>>  sysdeps/pthread/Makefile         |   7 +
>>  sysdeps/pthread/tst-atfork5.c    | 115 +++++++++++++
>>  sysdeps/pthread/tst-atfork5mod.c | 175 +++++++++++++++++++
>>  5 files changed, 464 insertions(+), 116 deletions(-)
>>  create mode 100644 sysdeps/pthread/tst-atfork5.c
>>  create mode 100644 sysdeps/pthread/tst-atfork5mod.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..d5d1df2d56a 100644
>> --- a/sysdeps/pthread/Makefile
>> +++ b/sysdeps/pthread/Makefile
>> @@ -348,6 +348,7 @@ tests += \
>>    tst-atfork2 \
>>    tst-atfork3 \
>>    tst-atfork4 \
>> +  tst-atfork5 \
>>    tst-create1 \
>>    tst-fini1 \
>>    tst-pt-tls4 \
>> @@ -364,6 +365,7 @@ modules-names += \
>>    tst-atfork2mod \
>>    tst-atfork3mod \
>>    tst-atfork4mod \
>> +  tst-atfork5mod \
>>    tst-create1mod \
>>    tst-fini1mod \
>>    tst-stack2-mod \
>> @@ -459,10 +461,15 @@ $(objpfx)tst-atfork4: $(shared-thread-library)
>>  LDFLAGS-tst-atfork4 = -rdynamic
>>  $(objpfx)tst-atfork4mod.so: $(shared-thread-library)
>>
>> +$(objpfx)tst-atfork5: $(shared-thread-library)
>> +tst-atfork5-ENV = LD_PRELOAD=$(objpfx)tst-atfork5mod.so
>> +$(objpfx)tst-atfork5mod.so: $(shared-thread-library)
>> +
>>  ifeq ($(build-shared),yes)
>>  $(objpfx)tst-atfork2.out: $(objpfx)tst-atfork2mod.so
>>  $(objpfx)tst-atfork3.out: $(objpfx)tst-atfork3mod.so
>>  $(objpfx)tst-atfork4.out: $(objpfx)tst-atfork4mod.so
>> +$(objpfx)tst-atfork5.out: $(objpfx)tst-atfork5mod.so
>>  endif
>>
>>  ifeq ($(build-shared),yes)
>> diff --git a/sysdeps/pthread/tst-atfork5.c b/sysdeps/pthread/tst-atfork5.c
>> new file mode 100644
>> index 00000000000..e3adfc9292d
>> --- /dev/null
>> +++ b/sysdeps/pthread/tst-atfork5.c
>> @@ -0,0 +1,115 @@
>> +/* 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.
>> +
>> +   The malloc replacement is provided by tst-atfork5mod.so through
>> +   LD_PRELOAD.  */
>> +
>> +#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 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.  */
>> +
>> +/* Synchronizes the NREG tf threads with the first prepare handler run, so
>> +   that they only start registering once the allocator lock is held by the
>> +   preloaded module's prepare handler.  */
>> +static pthread_barrier_t *window_barrier;
>> +
>> +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 (the module's
>> +     allocator lock held), so the first handler-list 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 (&reg_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)
>> +{
>> +  /* Check if tst-atfork5mod.so was preloaded.  */
>> +  void (*mod_prepare) (void) = xdlsym (RTLD_DEFAULT, "atfork5mod_prepare");
>> +  void (*mod_parent) (void) = xdlsym (RTLD_DEFAULT, "atfork5mod_parent");
>> +  void (*mod_child) (void) = xdlsym (RTLD_DEFAULT, "atfork5mod_child");
>> +  atomic_uint *malloc_count = xdlsym (RTLD_DEFAULT,
>> +                                     "atfork5mod_malloc_count");
>> +  window_barrier = xdlsym (RTLD_DEFAULT, "atfork5mod_window_barrier");
>> +
>> +  xpthread_barrier_init (window_barrier, NULL, NREG + 1);
>> +  TEST_COMPARE (pthread_atfork (mod_prepare, mod_parent, mod_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]);
>> +
>> +  TEST_VERIFY (atomic_load (malloc_count) > 0);
>> +
>> +  xpthread_barrier_destroy (window_barrier);
>> +  return 0;
>> +}
>> +
>> +#define TIMEOUT 8
>> +#include <support/test-driver.c>
>> diff --git a/sysdeps/pthread/tst-atfork5mod.c b/sysdeps/pthread/tst-atfork5mod.c
>> new file mode 100644
>> index 00000000000..bb59560db8f
>> --- /dev/null
>> +++ b/sysdeps/pthread/tst-atfork5mod.c
>> @@ -0,0 +1,175 @@
>> +/* Malloc replacement module for tst-atfork5 (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/>.  */
>> +
>> +/* Minimal malloc interposer, applied to tst-atfork5 through LD_PRELOAD:
>> +   every allocation takes ARENA_LOCK, and so does the fork prepare handler
>> +   below (registered by the test program via pthread_atfork).  */
>> +
>> +#include <dlfcn.h>
>> +#include <pthread.h>
>> +#include <stdatomic.h>
>> +#include <stdbool.h>
>> +#include <stdlib.h>
>> +#include <unistd.h>
>> +
>> +/* Microseconds the prepare handler holds ARENA_LOCK.  */
>> +#define WINDOW_US 4000
>> +
>> +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 *);
>> +
>> +/* Number of allocations that went through the interposer; the test program
>> +   uses it to check that this module was actually preloaded.  */
>> +atomic_uint atfork5mod_malloc_count;
>> +
>> +/* Small bump buffer to 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 *
>> +next_sym (const char *name)
>> +{
>> +  void *sym = dlsym (RTLD_NEXT, name);
>> +  if (sym == NULL)
>> +    abort ();
>> +  return sym;
>> +}
>> +
>> +static void
>> +init_real (void)
>> +{
>> +  if (real_malloc != NULL)
>> +    return;
>> +  in_dlsym = true;
>> +  real_malloc = next_sym ("malloc");
>> +  real_calloc = next_sym ("calloc");
>> +  real_realloc = next_sym ("realloc");
>> +  real_free = next_sym ("free");
>> +  in_dlsym = false;
>> +}
>> +
>> +void *
>> +malloc (size_t n)
>> +{
>> +  if (real_malloc == NULL)
>> +    {
>> +      if (in_dlsym)
>> +       return boot_alloc (n);
>> +      init_real ();
>> +    }
>> +  atomic_fetch_add (&atfork5mod_malloc_count, 1);
>> +  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 ();
>> +    }
>> +  atomic_fetch_add (&atfork5mod_malloc_count, 1);
>> +  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 ();
>> +  atomic_fetch_add (&atfork5mod_malloc_count, 1);
>> +  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 ();
>> +  pthread_mutex_lock (&arena_lock);
>> +  real_free (p);
>> +  pthread_mutex_unlock (&arena_lock);
>> +}
>> +
>> +/* Synchronizes the registering threads of the test program with the first
>> +   prepare handler run, so that they only start registering once ARENA_LOCK
>> +   is held by the prepare handler below.  Initialized by the test program.  */
>> +pthread_barrier_t atfork5mod_window_barrier;
>> +static atomic_int synced;
>> +
>> +void
>> +atfork5mod_prepare (void)
>> +{
>> +  pthread_mutex_lock (&arena_lock);
>> +  /* Release the registering threads on the first fork only; on later forks
>> +     they are no longer waiting on the barrier.  */
>> +  if (!atomic_exchange (&synced, 1))
>> +    pthread_barrier_wait (&atfork5mod_window_barrier);
>> +  usleep (WINDOW_US);
>> +}
>> +
>> +void
>> +atfork5mod_parent (void)
>> +{
>> +  pthread_mutex_unlock (&arena_lock);
>> +}
>> +
>> +void
>> +atfork5mod_child (void)
>> +{
>> +  pthread_mutex_unlock (&arena_lock);
>> +}
>> +
>> +__attribute__ ((constructor))
>> +static void
>> +init (void)
>> +{
>> +  init_real ();
>> +}
>> --
>> 2.53.0
>>
> 
> 



More information about the Libc-alpha mailing list