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

H.J. Lu hjl.tools@gmail.com
Tue Sep 15 23:26:18 GMT 2026


On Tue, Sep 15, 2026 at 7:22 PM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
>
>
>
> 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.

Does it fix any jemalloc related bugs?

> >
> >> 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
> >>
> >
> >
>


-- 
H.J.


More information about the Libc-alpha mailing list