[PATCH] Do not translate ENOMEM to EAGAIN in pthread_create

Florian Weimer fweimer@redhat.com
Wed Jul 8 11:04:43 GMT 2026


Applications may want to retry on EAGAIN errors, but not on ENOMEM
errors.  If we translate ENOMEM errors, this is not possible.

---
 NEWS                                |   6 ++
 htl/pt-create.c                     |   2 -
 manual/threads.texi                 |  22 +++++-
 nptl/pthread_create.c               |  30 +++-----
 sysdeps/pthread/Makefile            |   1 +
 sysdeps/pthread/tst-basic7.c        |  15 ++--
 sysdeps/pthread/tst-thread-basic8.c | 132 ++++++++++++++++++++++++++++++++++++
 7 files changed, 174 insertions(+), 34 deletions(-)

diff --git a/NEWS b/NEWS
index f9d90c5194..02eed38112 100644
--- a/NEWS
+++ b/NEWS
@@ -57,6 +57,12 @@ Deprecated and removed features, and other changes affecting compatibility:
   remaining behavior was to suppress the link-time warnings on the NSS
   interface functions in libc.a, which are now emitted unconditionally.
 
+* The pthread_create function no longer translates ENOMEM errors to
+  EAGAIN errors.  On some systems, true EAGAIN errors can be caused by
+  temporary conditions and a repeated pthread_create call may succeed.
+  In contrast, ENOMEM errors are likely to persist until some memory is
+  deallocated.
+
 Changes to build and runtime requirements:
 
   [Add changes to build and runtime requirements here]
diff --git a/htl/pt-create.c b/htl/pt-create.c
index a66b6ebdcf..6c9d5912a4 100644
--- a/htl/pt-create.c
+++ b/htl/pt-create.c
@@ -104,8 +104,6 @@ __libc_pthread_create (pthread_t * thread, const pthread_attr_t * attr,
   err = __pthread_create_internal (&pthread, attr, start_routine, arg);
   if (!err)
     *thread = pthread->thread;
-  else if (err == ENOMEM)
-    err = EAGAIN;
 
   return err;
 }
diff --git a/manual/threads.texi b/manual/threads.texi
index 0331599bc0..2f43f7f917 100644
--- a/manual/threads.texi
+++ b/manual/threads.texi
@@ -593,7 +593,27 @@ This function creates a new thread with attributes @var{attr}.  This
 thread will call @var{start_routine} and pass it @var{arg}.  If
 @var{start_routine} returns, the thread will exit and the return value
 will become the thread's exit value.  The new thread's ID is stored in
-@var{newthread}.  Returns 0 on success.
+@var{newthread}.  Returns 0 on success, and an error code on failure.
+Among the failure codes are:
+
+@table @code
+@item EAGAIN
+The thread could not be created because the maximum thread count for the
+process, user, or system has been reached.  On Linux, this error can be
+spurious, and calling @code{pthread_create} again may succeed although
+no threads have exited since the previous call.
+
+@item ENOMEM
+Memory for the new thread could not be allocated.  This can refer to
+memory for the stack, thread-local storage, or administrative data.
+
+POSIX suggests reporting these error conditions as @code{EAGAIN}, but
+@theglibc{} uses @code{ENOMEM} instead.  Out-of-memory conditions
+typically are more persistent than @code{EAGAIN} errors, which can be
+spurious.  Separate error codes allow applications to handle these
+conditions differently.
+@end table
+
 @manpagefunctionstub{pthread_create, 3}
 @end deftypefun
 
diff --git a/nptl/pthread_create.c b/nptl/pthread_create.c
index fcb06c68c7..3e3c3b1016 100644
--- a/nptl/pthread_create.c
+++ b/nptl/pthread_create.c
@@ -214,7 +214,7 @@ late_init (void)
    action before calling the user entry point.  The goal of all of this
    is to ensure that the required initial thread attributes are applied
    (by the creating thread) before the new thread runs user code.  Note
-   that the the functions pthread_getschedparam, pthread_setschedparam,
+   that the functions pthread_getschedparam, pthread_setschedparam,
    pthread_setschedprio, __pthread_tpp_change_priority, and
    __pthread_current_priority reuse the same lock, PD->lock, for a
    similar purpose e.g. synchronizing the setting of similar thread
@@ -224,10 +224,10 @@ late_init (void)
    it saves space.
 
    The return value is zero for success or an errno code for failure.
-   If the return value is ENOMEM, that will be translated to EAGAIN,
-   so create_thread need not do that.  On failure, *THREAD_RAN should
-   be set to true iff the thread actually started up but before calling
-   the user code (*PD->start_routine).  */
+   On failure, *THREAD_RAN should be set to true iff the thread has
+   started and the failure occurred before running user code
+   (*PD->start_routine); therefore, the thread needs to be joined
+   before reporting the error to the application.  */
 
 static int _Noreturn start_thread (void *arg);
 
@@ -235,7 +235,7 @@ static int create_thread (struct pthread *pd, const struct pthread_attr *attr,
 			  bool *stopped_start, void *stackaddr,
 			  size_t stacksize, bool *thread_ran)
 {
-  /* Determine whether the newly created threads has to be started
+  /* Determine whether the newly created thread has to be started
      stopped since we have to set the scheduling parameters or set the
      affinity.  */
   bool need_setaffinity = (attr != NULL && attr->extension != NULL
@@ -677,18 +677,12 @@ __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
     }
 
   struct pthread *pd = NULL;
-  int err = allocate_stack (iattr, &pd, &stackaddr, &stacksize);
-  int retval = 0;
+  int retval = allocate_stack (iattr, &pd, &stackaddr, &stacksize);
 
-  if (__glibc_unlikely (err != 0))
+  if (__glibc_unlikely (retval != 0))
     /* Something went wrong.  Maybe a parameter of the attributes is
-       invalid or we could not allocate memory.  Note we have to
-       translate error codes.  */
-    {
-      retval = err == ENOMEM ? EAGAIN : err;
-      goto out;
-    }
-
+       invalid or we could not allocate memory.  */
+    goto out;
 
   /* Initialize the TCB.  All initializations with zero should be
      performed in 'get_cached_stack'.  This way we avoid doing this if
@@ -893,10 +887,6 @@ __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
 
       /* Free the resources.  */
       __nptl_deallocate_stack (pd);
-
-      /* We have to translate error codes.  */
-      if (retval == ENOMEM)
-	retval = EAGAIN;
     }
   else
     {
diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile
index d0f3cd59ac..3e8fd1de32 100644
--- a/sysdeps/pthread/Makefile
+++ b/sysdeps/pthread/Makefile
@@ -275,6 +275,7 @@ tests += \
   tst-stdio2 \
   tst-thrd-detach \
   tst-thrd-sleep \
+  tst-thread-basic8 \
   tst-tsd1 \
   tst-tsd2 \
   tst-tsd5 \
diff --git a/sysdeps/pthread/tst-basic7.c b/sysdeps/pthread/tst-basic7.c
index 26a599c178..0db7ccb352 100644
--- a/sysdeps/pthread/tst-basic7.c
+++ b/sysdeps/pthread/tst-basic7.c
@@ -7,6 +7,7 @@
 #include <unistd.h>
 #include <sys/mman.h>
 #include <sys/resource.h>
+#include <support/check.h>
 
 static void use_stack (size_t needed);
 
@@ -51,7 +52,6 @@ child (void *arg)
 static int
 do_test (void)
 {
-  int err;
   pthread_t tid;
 
   /* Allocate the memory needed for the stack.  */
@@ -63,17 +63,10 @@ do_test (void)
 
   use_up_memory ();
 
-  err = pthread_create (&tid, NULL, child, NULL);
-  if (err != 0)
-    {
-      printf ("pthread_create returns %d: %s\n", err,
-	      err == EAGAIN ? "OK" : "FAIL");
-      return err != EAGAIN;
-    }
+  errno = pthread_create (&tid, NULL, child, NULL);
+  TEST_COMPARE (errno, ENOMEM);
 
-  /* We did not fail to allocate memory despite the preparation.  Oh well.  */
   return 0;
 }
 
-#define TEST_FUNCTION do_test ()
-#include "../test-skeleton.c"
+#include <support/test-driver.c>
diff --git a/sysdeps/pthread/tst-thread-basic8.c b/sysdeps/pthread/tst-thread-basic8.c
new file mode 100644
index 0000000000..18fe298a20
--- /dev/null
+++ b/sysdeps/pthread/tst-thread-basic8.c
@@ -0,0 +1,132 @@
+/* Check that repeated pthread_create eventually fails with EAGAIN.
+   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/>.  */
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xstdio.h>
+#include <support/xthread.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/resource.h>
+
+/* Used to block just-launched threads in a barrier, to avoid running
+   too many threads in parallel.  */
+static pthread_barrier_t barrier;
+
+/* List of threads that have been created.  */
+struct thr
+{
+  pthread_t id;
+  struct thr *next;
+};
+
+static void *
+thread_routine (void *ignored)
+{
+  xpthread_barrier_wait (&barrier);
+  pause ();
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  /* Reduce the number of supported threads.  */
+  {
+    /* Compute a target thread limit based on the number of running
+       threads on the system.  500 is just a random guess if the value
+       cannot be read.  */
+    int target = 500;
+    {
+      FILE *fp = fopen ("/proc/loadavg", "r");
+      if (fp != NULL)
+        {
+          float ignored;
+          int count;
+          if (fscanf (fp, "%f %f %f %f/%d",
+                      &ignored, &ignored, &ignored, &ignored, &count)
+              == 5)
+            target = count + 100;
+          xfclose (fp);
+        }
+    }
+
+    struct rlimit rl;
+    if (getrlimit (RLIMIT_NPROC, &rl) != 0)
+      FAIL_EXIT1 ("getrlimit (RLIMIT_NPROC): %m");
+    if (rl.rlim_cur == RLIM_INFINITY || rl.rlim_cur > target)
+      {
+        printf ("info: setting thread limit to %d\n", target);
+        rl.rlim_cur = target;
+        if (setrlimit (RLIMIT_NPROC, &rl) != 0)
+          FAIL_EXIT1 ("setrlimit (RLIMIT_NPROC): %m");
+      }
+  }
+
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  support_set_small_thread_stack_size (&attr, false);
+
+  xpthread_barrier_init (&barrier, NULL, 2);
+
+  struct thr *list = NULL;
+
+  /* Create threads until creation fails.  This typically fails sooner
+     than the target above because the user already has some number of
+     threads running.  */
+  while (true)
+    {
+      pthread_t id;
+      errno = pthread_create (&id, &attr, thread_routine, NULL);
+      if (errno == EAGAIN)
+        break;
+      /* Only EAGAIN errors are expected.  With a small stack size,
+         expect to run out of threads before hitting the memory
+         allocation failure.  */
+      if (errno > 0)
+        FAIL_EXIT1 ("pthread_create: %m");
+
+      struct thr *e = xmalloc (sizeof (*e));
+      e->id = id;
+      e->next = list;
+      list = e;
+
+      xpthread_barrier_wait (&barrier);
+    }
+
+  /* Terminate the threads.  */
+  while (list != NULL)
+    {
+      struct thr *e = list;
+      list = list->next;
+
+      xpthread_cancel (e->id);
+      TEST_VERIFY (xpthread_join (e->id) == PTHREAD_CANCELED);
+      free (e);
+    }
+
+  xpthread_barrier_destroy (&barrier);
+  xpthread_attr_destroy (&attr);
+  return 0;
+}
+
+#include <support/test-driver.c>

base-commit: 0f61d77aef1bcd5977211e87e0496768f5650c0e



More information about the Libc-alpha mailing list