[RFC] posix: Make system, popen, and wordexp try pidfd_spawn first (BZ 34001)

Message ID 20260429200041.3051117-1-adhemerval.zanella@linaro.org (mailing list archive)
State New
Headers
Series [RFC] posix: Make system, popen, and wordexp try pidfd_spawn first (BZ 34001) |

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

Commit Message

Adhemerval Zanella Netto April 29, 2026, 8 p.m. UTC
  This patch updates system, popen, and wordexp to use a new internal
process management abstraction (__spawn_process_create, __spawn_process_wait,
and __spawn_process_kill) that leverages pidfd_spawn on supported Linux
kernels before falling back to traditional posix_spawn.

The internal process_create_id_t wraps both pid_t and int (for pidfd)
and uses the MSB to differentiate a returned pidfd from a standard PID.
This avoid the need to use extra word to track which type is used and
allows to use the interface as drop-in replacement for posix_spawn
and waitpid.

Because pidfd_spawn allocates a new file descriptor in the calling
process for the pidfd, this change alters the resource consumption semantics
of these functions.  The __spawn_process_create handles if pidfd_spawns
return EMFILE or ENFILE, but if the pidfd_spawns is successful a
file descriptor is consumed and it counts for the process limits.

Checked on x86_64-linux-gnu and i686-linux-gnu.
---
 include/clone_internal.h                      |   1 +
 include/spawn.h                               |  33 +++++
 libio/iopopen.c                               |  23 ++--
 posix/Makefile                                |   9 ++
 posix/tst-spawn_process-fd-exhaustion.c       | 120 +++++++++++++++++
 posix/wordexp.c                               |  39 +++---
 sysdeps/generic/spawn_process.c               |  43 +++++++
 sysdeps/posix/system.c                        |  25 ++--
 .../unix/sysv/linux/include/bits/spawn_ext.h  |  11 ++
 sysdeps/unix/sysv/linux/not-errno.h           |  11 ++
 sysdeps/unix/sysv/linux/pidfd_spawn.c         |  10 +-
 sysdeps/unix/sysv/linux/spawn_process.c       | 121 ++++++++++++++++++
 12 files changed, 404 insertions(+), 42 deletions(-)
 create mode 100644 posix/tst-spawn_process-fd-exhaustion.c
 create mode 100644 sysdeps/generic/spawn_process.c
 create mode 100644 sysdeps/unix/sysv/linux/include/bits/spawn_ext.h
 create mode 100644 sysdeps/unix/sysv/linux/spawn_process.c
  

Comments

Florian Weimer May 4, 2026, 1:49 p.m. UTC | #1
* Adhemerval Zanella:

> This patch updates system, popen, and wordexp to use a new internal
> process management abstraction (__spawn_process_create, __spawn_process_wait,
> and __spawn_process_kill) that leverages pidfd_spawn on supported Linux
> kernels before falling back to traditional posix_spawn.

How does this change the way SIGCHLD is delivered?

Thanks,
Florian
  
Adhemerval Zanella Netto May 4, 2026, 1:58 p.m. UTC | #2
On 04/05/26 10:49, Florian Weimer wrote:
> * Adhemerval Zanella:
> 
>> This patch updates system, popen, and wordexp to use a new internal
>> process management abstraction (__spawn_process_create, __spawn_process_wait,
>> and __spawn_process_kill) that leverages pidfd_spawn on supported Linux
>> kernels before falling back to traditional posix_spawn.
> 
> How does this change the way SIGCHLD is delivered?
It uses pidfd_spawn internally, which still uses '.exit_signal = SIGCHLD,'. We 
can an internal-only interface to remove it, but I am not sure if this is really
required (afaik waitid (PIDFD, ...) will signal an error if the process is
already reaped).

And this lead to another question: should we omit SIGCHLD on pidfd_spawn?
  
Cristian Rodríguez May 5, 2026, 1:05 p.m. UTC | #3
On Mon, May 4, 2026 at 9:58 AM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
 (afaik waitid (PIDFD, ...) will signal an error if the process is
> already reaped).

Yes, it will return ECHILD..afaik.
  
Adhemerval Zanella Netto May 5, 2026, 1:07 p.m. UTC | #4
On 05/05/26 10:05, Cristian Rodríguez wrote:
> On Mon, May 4, 2026 at 9:58 AM Adhemerval Zanella Netto
> <adhemerval.zanella@linaro.org> wrote:
>  (afaik waitid (PIDFD, ...) will signal an error if the process is
>> already reaped).
> 
> Yes, it will return ECHILD..afaik.

And I am not sure we can change it, because it will subtly change the functions
semantics and we will always need a clone fallback (that always send the signal)
for ABI that do not have clone3 and on kernel that do not support it.
  
Florian Weimer May 5, 2026, 3:03 p.m. UTC | #5
* Adhemerval Zanella Netto:

> On 05/05/26 10:05, Cristian Rodríguez wrote:
>> On Mon, May 4, 2026 at 9:58 AM Adhemerval Zanella Netto
>> <adhemerval.zanella@linaro.org> wrote:
>>  (afaik waitid (PIDFD, ...) will signal an error if the process is
>>> already reaped).
>> 
>> Yes, it will return ECHILD..afaik.
>
> And I am not sure we can change it, because it will subtly change the
> functions semantics and we will always need a clone fallback (that
> always send the signal) for ABI that do not have clone3 and on kernel
> that do not support it.

I think we could perhaps hide the process launch for wordexp, but not
for system and popen.  For system and popen, some applications expect to
see signals.

Thanks,
Florian
  
Adhemerval Zanella Netto May 5, 2026, 3:20 p.m. UTC | #6
On 05/05/26 12:03, Florian Weimer wrote:
> * Adhemerval Zanella Netto:
> 
>> On 05/05/26 10:05, Cristian Rodríguez wrote:
>>> On Mon, May 4, 2026 at 9:58 AM Adhemerval Zanella Netto
>>> <adhemerval.zanella@linaro.org> wrote:
>>>  (afaik waitid (PIDFD, ...) will signal an error if the process is
>>>> already reaped).
>>>
>>> Yes, it will return ECHILD..afaik.
>>
>> And I am not sure we can change it, because it will subtly change the
>> functions semantics and we will always need a clone fallback (that
>> always send the signal) for ABI that do not have clone3 and on kernel
>> that do not support it.
> 
> I think we could perhaps hide the process launch for wordexp, but not
> for system and popen.  For system and popen, some applications expect to
> see signals.
Indeed the wordexp is an implementation detail to call the shell.

But back to the main RFC, would this be an acceptable change?
  
Cristian Rodriguez May 8, 2026, 1:26 p.m. UTC | #7
On Tue, May 5, 2026 at 11:20 AM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
>
>
>
> On 05/05/26 12:03, Florian Weimer wrote:
> > * Adhemerval Zanella Netto:
> >

> But back to the main RFC, would this be an acceptable change?

It would be very nice if someone could ack this.
It resolves PID reuse problems that calling applications can't actually fix.
it is possible for example that system() or wordexp calls
`__kill_noerrno` on a PID that has already been reused if either
the system has a heavy process creation load or is only slightly
loaded and uses the default or low pid.max.
  

Patch

diff --git a/include/clone_internal.h b/include/clone_internal.h
index 567160ebb5..001797fccd 100644
--- a/include/clone_internal.h
+++ b/include/clone_internal.h
@@ -1,6 +1,7 @@ 
 #ifndef _CLONE_INTERNAL_H
 #define _CLONE_INTERNAL_H
 
+#include <stdbool.h>
 #include <clone3.h>
 
 /* The clone3 syscall provides a superset of the functionality of the clone
diff --git a/include/spawn.h b/include/spawn.h
index 4a0b1849da..95375337e4 100644
--- a/include/spawn.h
+++ b/include/spawn.h
@@ -2,6 +2,8 @@ 
 #include <posix/spawn.h>
 
 # ifndef _ISOMAC
+#  include <sys/wait.h>
+
 __typeof (posix_spawn) __posix_spawn;
 libc_hidden_proto (__posix_spawn)
 
@@ -35,5 +37,36 @@  __typeof (posix_spawnattr_setsigdefault) __posix_spawnattr_setsigdefault
 __typeof (posix_spawnattr_setsigmask) __posix_spawnattr_setsigmask
   attribute_hidden;
 
+typedef int process_create_id_t;
+_Static_assert (sizeof (process_create_id_t) == sizeof (pid_t),
+		"process_create_id_t must have same size as pid_t");
+
+/* Create a new process using pidfd_spawn or posix_spawn as a fallback, and
+   return an identifier that should be only be used with __spawn_process_wait.
+   The identifier is not changed if the process creation fails and the
+   function returns the same error code as {pidfd,posix}_spawn.  */
+int __spawn_process_create (process_create_id_t *,
+			    const char *__restric__,
+			    const posix_spawn_file_actions_t *__restrict,
+			    const posix_spawnattr_t *__restrict,
+			    char *const [__restrict_arr],
+			    char *const [__restrict_arr])
+     attribute_hidden;
+
+/* Wait for a process created with process_create_id_t, and return the status
+   code as for waitpid in second argument.  The third argument is the options
+   to be used, for instance WNOHANG.
+
+   It returns either the process id returned by __spawn_process_create for
+   the case of pidfd, or the pid_t if the fallback is used.  This semantic
+   allows to use this in place of waitpid calls.  */
+process_create_id_t __spawn_process_wait (process_create_id_t, int *, int)
+     attribute_hidden;
+
+/* Send a signal to the created process using either pidfd_send_signal or
+   kill.  */
+int __spawn_process_kill (process_create_id_t, int)
+     attribute_hidden;
+
 # endif /* !_ISOMAC  */
 #endif /* spawn.h  */
diff --git a/libio/iopopen.c b/libio/iopopen.c
index 37b6b1386b..9f39dc858f 100644
--- a/libio/iopopen.c
+++ b/libio/iopopen.c
@@ -40,7 +40,7 @@  struct _IO_proc_file
 {
   struct _IO_FILE_plus file;
   /* Following fields must match those in class procbuf (procbuf.h) */
-  pid_t pid;
+  process_create_id_t procid;
   struct _IO_proc_file *next;
 };
 typedef struct _IO_proc_file _IO_proc_file;
@@ -106,9 +106,16 @@  spawn_process (posix_spawn_file_actions_t *fa, FILE *fp, const char *command,
 	}
     }
 
-  err = __posix_spawn (&((_IO_proc_file *) fp)->pid, _PATH_BSHELL, fa, NULL,
-		       (char *const[]){ (char*) "sh", (char*) "-c", (char*) "--",
-		       (char *) command, NULL }, __environ);
+  err = __spawn_process_create (&((_IO_proc_file *) fp)->procid,
+				_PATH_BSHELL,
+				fa,
+				NULL,
+				(char *const[]){ (char*) "sh",
+						 (char*) "-c",
+						 (char*) "--",
+						 (char *) command,
+						 NULL },
+				__environ);
   if (err != 0)
     return err;
 
@@ -274,7 +281,6 @@  _IO_new_proc_close (FILE *fp)
   /* This is not name-space clean. FIXME! */
   int wstatus;
   _IO_proc_file **ptr = &proc_file_chain;
-  pid_t wait_pid;
   int status = -1;
 
   /* Unlink from proc_file_chain. */
@@ -306,11 +312,12 @@  _IO_new_proc_close (FILE *fp)
     {
       int state;
       __pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &state);
-      wait_pid = __waitpid (((_IO_proc_file *) fp)->pid, &wstatus, 0);
+      status =__spawn_process_wait (((_IO_proc_file *) fp)->procid,
+				    &wstatus, 0);
       __pthread_setcancelstate (state, NULL);
     }
-  while (wait_pid == -1 && errno == EINTR);
-  if (wait_pid == -1)
+  while (status == -1 && errno == EINTR);
+  if (status == -1)
     return -1;
   return wstatus;
 }
diff --git a/posix/Makefile b/posix/Makefile
index 0fa532396f..af5a5d9960 100644
--- a/posix/Makefile
+++ b/posix/Makefile
@@ -155,6 +155,7 @@  routines := \
   spawn_faction_addtcsetpgrp_np \
   spawn_faction_destroy \
   spawn_faction_init \
+  spawn_process \
   spawn_valid_fd \
   spawnattr_destroy \
   spawnattr_getdefault \
@@ -350,7 +351,12 @@  tests += \
   # tests
 endif
 
+tests-static-internal := \
+  tst-spawn_process-fd-exhaustion \
+  # tests-static-internal
+
 tests-internal := \
+  $(tests-static-internal)\
   bug-regex5 \
   bug-regex20 \
   bug-regex33 \
@@ -397,6 +403,7 @@  tests += \
 endif
 
 tests-static = \
+  $(tests-static-internal) \
   tst-exec-static \
   tst-libc-message \
   tst-spawn-static \
@@ -803,3 +810,5 @@  tst-wordexp-reuse-ENV += MALLOC_TRACE=$(objpfx)tst-wordexp-reuse.mtrace \
 $(objpfx)tst-wordexp-reuse-mem.out: $(objpfx)tst-wordexp-reuse.out
 	$(common-objpfx)malloc/mtrace $(objpfx)tst-wordexp-reuse.mtrace > $@; \
 	$(evaluate-test)
+
+CFLAGS-tst-spawn_process-fd-exhaustion.c += -DOBJPFX=\"$(objpfx)\"
diff --git a/posix/tst-spawn_process-fd-exhaustion.c b/posix/tst-spawn_process-fd-exhaustion.c
new file mode 100644
index 0000000000..98c1fea515
--- /dev/null
+++ b/posix/tst-spawn_process-fd-exhaustion.c
@@ -0,0 +1,120 @@ 
+/* Check if __spawn_process_create works if there is no available
+   file descriptor.
+   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 <fcntl.h>
+#include <intprops.h>
+#include <paths.h>
+#include <spawn.h>
+#include <stdlib.h>
+#include <sys/resource.h>
+
+#include <support/check.h>
+#include <support/temp_file.h>
+#include <support/xunistd.h>
+
+#include <stdio.h>
+
+static const char pidfile[] = OBJPFX "tst-spawn_process-fd-exhaustion.pid";
+
+static int
+do_test (void)
+{
+  struct rlimit rl;
+  int max_fd = 24;
+
+  if (getrlimit (RLIMIT_NOFILE, &rl) == -1)
+    FAIL_EXIT1 ("getrlimit (RLIMIT_NOFILE): %m");
+
+  max_fd = (rl.rlim_cur < max_fd ? rl.rlim_cur : max_fd);
+  rl.rlim_cur = max_fd;
+
+  if (setrlimit (RLIMIT_NOFILE, &rl) == -1)
+    FAIL_EXIT1 ("setrlimit (RLIMIT_NOFILE): %m");
+
+  /* Exhauste the file descriptor limit with temporary files.  */
+  int files[max_fd];
+  int nfiles = 0;
+  for (; nfiles < max_fd; nfiles++)
+    {
+      int fd = create_temp_file ("tst-spawn_process-fd-exhaustion.pid.", NULL);
+      if (fd == -1)
+	{
+	  if (errno != EMFILE)
+	    FAIL_EXIT1 ("create_temp_file: %m");
+	  break;
+	}
+      int flags = fcntl (fd, F_GETFD, 0);
+      TEST_VERIFY_EXIT (flags != -1);
+      TEST_VERIFY_EXIT (fcntl (fd, F_SETFD, flags | FD_CLOEXEC) != -1);
+      files[nfiles] = fd;
+    }
+  TEST_VERIFY_EXIT (nfiles != 0);
+
+  process_create_id_t pid;
+  {
+    posix_spawn_file_actions_t fa;
+    TEST_COMPARE (posix_spawn_file_actions_init (&fa), 0);
+    TEST_COMPARE (posix_spawn_file_actions_addopen (&fa, STDOUT_FILENO,
+						    pidfile,
+						    O_WRONLY| O_CREAT
+						    | O_TRUNC,
+						    0644), 0);
+
+    TEST_COMPARE (posix_spawn_file_actions_adddup2 (&fa, STDOUT_FILENO,
+						    STDERR_FILENO), 0);
+    char *spawn_argv[] =
+      {
+	(char *) _PATH_BSHELL,
+	(char *) "-c",
+	(char *) "echo $$",
+	NULL
+      };
+    int r = __spawn_process_create (&pid, _PATH_BSHELL, &fa, NULL,
+				    spawn_argv, NULL);
+    TEST_COMPARE (r, 0);
+
+    int status;
+    TEST_COMPARE (__spawn_process_wait (pid, &status, 0), pid);
+    TEST_COMPARE (WIFEXITED (status), 1);
+    TEST_COMPARE (WEXITSTATUS (status), 0);
+  }
+
+  for (int i=0; i<nfiles; i++)
+    xclose (files[i]);
+
+  {
+    int pidfd = xopen (pidfile, O_RDONLY, 0);
+
+    char buf[INT_BUFSIZE_BOUND (pid_t)];
+    ssize_t n = read (pidfd, buf, sizeof (buf));
+    TEST_VERIFY (n < sizeof buf && n >= 0);
+
+    /* We only expect to read the PID.  */
+    char *endp;
+    long int rpid = strtol (buf, &endp, 10);
+    TEST_VERIFY (*endp == '\n' && endp != buf);
+
+    TEST_COMPARE (rpid, pid);
+  }
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/posix/wordexp.c b/posix/wordexp.c
index 4a8541add4..a4d7040612 100644
--- a/posix/wordexp.c
+++ b/posix/wordexp.c
@@ -804,10 +804,11 @@  parse_arith (char **word, size_t *word_length, size_t *max_length,
 #include <malloc/dynarray-skeleton.c>
 
 /* Function called by child process in exec_comm() */
-static pid_t
-exec_comm_child (char *comm, int *fildes, bool showerr, bool noexec)
+static bool
+exec_comm_child (process_create_id_t *procid, char *comm, int *fildes,
+		 bool showerr, bool noexec)
 {
-  pid_t pid = -1;
+  bool r = false;
 
   /* Execute the command, or just check syntax?  */
   const char *args[] = { _PATH_BSHELL, noexec ? "-nc" : "-c", comm, NULL };
@@ -855,17 +856,17 @@  exec_comm_child (char *comm, int *fildes, bool showerr, bool noexec)
 	goto out;
     }
 
-  /* pid is not set if posix_spawn fails, so it keep the original value
-     of -1.  */
-  __posix_spawn (&pid, _PATH_BSHELL, &fa, NULL, (char *const *) args,
-		 recreate_env ? strlist_begin (&newenv) : __environ);
+  r = __spawn_process_create (procid, _PATH_BSHELL, &fa, NULL,
+			      (char *const *) args,
+			      recreate_env
+			      ? strlist_begin (&newenv) : __environ) == 0;
 
   strlist_free (&newenv);
 
 out:
   __posix_spawn_file_actions_destroy (&fa);
 
-  return pid;
+  return r;
 }
 
 /* Function to execute a command and retrieve the results */
@@ -882,7 +883,7 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
   int status = 0;
   size_t maxnewlines = 0;
   char buffer[bufsize];
-  pid_t pid;
+  process_create_id_t pid;
   bool noexec = false;
 
   /* Do nothing if command substitution should not succeed.  */
@@ -897,9 +898,8 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
     return WRDE_NOSPACE;
 
  again:
-  pid = exec_comm_child (comm, fildes, noexec ? false : flags & WRDE_SHOWERR,
-			 noexec);
-  if (pid < 0)
+  if (!exec_comm_child (&pid, comm, fildes,
+			noexec ? false : flags & WRDE_SHOWERR, noexec))
     {
       __close (fildes[0]);
       __close (fildes[1]);
@@ -908,7 +908,7 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
 
   /* If we are just testing the syntax, only wait.  */
   if (noexec)
-    return (TEMP_FAILURE_RETRY (__waitpid (pid, &status, 0)) == pid
+    return (TEMP_FAILURE_RETRY (__spawn_process_wait (pid, &status, 0)) == pid
 	    && status != 0) ? WRDE_SYNTAX : 0;
 
   __close (fildes[1]);
@@ -925,8 +925,10 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
 	      /* If read returned 0 then the process has closed its
 		 stdout.  Don't use WNOHANG in that case to avoid busy
 		 looping until the process eventually exits.  */
-	      if (TEMP_FAILURE_RETRY (__waitpid (pid, &status,
-						 buflen == 0 ? 0 : WNOHANG))
+	      if (TEMP_FAILURE_RETRY (__spawn_process_wait (pid,
+							    &status,
+							    buflen == 0
+							    ? 0 : WNOHANG))
 		  == 0)
 		continue;
 	      if ((buflen = TEMP_FAILURE_RETRY (__read (fildes[0], buffer,
@@ -960,8 +962,9 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
 	      /* If read returned 0 then the process has closed its
 		 stdout.  Don't use WNOHANG in that case to avoid busy
 		 looping until the process eventually exits.  */
-	      if (TEMP_FAILURE_RETRY (__waitpid (pid, &status,
-						 buflen == 0 ? 0 : WNOHANG))
+	      if (TEMP_FAILURE_RETRY (__spawn_process_wait (pid, &status,
+							    buflen == 0
+							    ? 0 : WNOHANG))
 		  == 0)
 		continue;
 	      if ((buflen = TEMP_FAILURE_RETRY (__read (fildes[0], buffer,
@@ -1094,7 +1097,7 @@  exec_comm (char *comm, char **word, size_t *word_length, size_t *max_length,
 
 no_space:
   __kill (pid, SIGKILL);
-  TEMP_FAILURE_RETRY (__waitpid (pid, NULL, 0));
+  TEMP_FAILURE_RETRY (__spawn_process_wait (pid, NULL, 0));
   __close (fildes[0]);
   return WRDE_NOSPACE;
 }
diff --git a/sysdeps/generic/spawn_process.c b/sysdeps/generic/spawn_process.c
new file mode 100644
index 0000000000..5ca8d6260d
--- /dev/null
+++ b/sysdeps/generic/spawn_process.c
@@ -0,0 +1,43 @@ 
+/* Internal implementation of __spawn_process*.  Generic implementation.
+   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 <spawn.h>
+#include <not-errno.h>
+
+int
+__spawn_process_create (process_create_id_t *procid,
+			const char *path,
+			const posix_spawn_file_actions_t *facts,
+			const posix_spawnattr_t *attr,
+			char *const argv[],
+			char *const envp[])
+{
+  return __posix_spawn (procid, path, facts, attr, argv, envp);
+}
+
+int
+__spawn_process_kill (process_create_id_t procid, int signo)
+{
+  return __kill (procid, signo);
+}
+
+process_create_id_t
+__spawn_process_wait (process_create_id_t procid, int *wstatus, int options)
+{
+  return __waitpid (procid, wstatus, options | WEXITED);
+}
diff --git a/sysdeps/posix/system.c b/sysdeps/posix/system.c
index d01ee518ae..73fb3b3ec1 100644
--- a/sysdeps/posix/system.c
+++ b/sysdeps/posix/system.c
@@ -45,7 +45,7 @@ 
    last thread will restore them.
 
    Cancellation handling is done with thread cancellation clean-up handlers
-   on waitpid call.  */
+   on __spawn_process_wait call.  */
 
 #ifdef _LIBC_REENTRANT
 static struct sigaction intr, quit;
@@ -71,7 +71,7 @@  struct cancel_handler_args
 {
   struct sigaction *quit;
   struct sigaction *intr;
-  pid_t pid;
+  process_create_id_t pid;
 };
 
 static void
@@ -79,11 +79,11 @@  cancel_handler (void *arg)
 {
   struct cancel_handler_args *args = (struct cancel_handler_args *) (arg);
 
-  __kill_noerrno (args->pid, SIGKILL);
+  __spawn_process_kill (args->pid, SIGKILL);
 
   int state;
   __pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &state);
-  TEMP_FAILURE_RETRY (__waitpid (args->pid, NULL, 0));
+  TEMP_FAILURE_RETRY (__spawn_process_wait (args->pid, NULL, 0));
   __pthread_setcancelstate (state, NULL);
 
   DO_LOCK ();
@@ -102,7 +102,7 @@  do_system (const char *line)
 {
   int status = -1;
   int ret;
-  pid_t pid;
+  process_create_id_t pid;
   struct sigaction sa;
 #ifndef _LIBC_REENTRANT
   struct sigaction intr, quit;
@@ -144,12 +144,13 @@  do_system (const char *line)
   __posix_spawnattr_setflags (&spawn_attr,
 			      POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);
 
-  ret = __posix_spawn (&pid, SHELL_PATH, NULL, &spawn_attr,
-		       (char *const[]){ (char *) SHELL_NAME,
-					(char *) "-c",
-					(char *) "--",
-					(char *) line, NULL },
-		       __environ);
+  ret = __spawn_process_create (&pid, SHELL_PATH, NULL, &spawn_attr,
+				(char *const[]){ (char *) SHELL_NAME,
+						 (char *) "-c",
+						 (char *) "--",
+						 (char *) line,
+						 NULL },
+				__environ);
   __posix_spawnattr_destroy (&spawn_attr);
 
   if (ret == 0)
@@ -169,7 +170,7 @@  do_system (const char *line)
       /* Note the system() is a cancellation point.  But since we call
 	 waitpid() which itself is a cancellation point we do not
 	 have to do anything here.  */
-      if (TEMP_FAILURE_RETRY (__waitpid (pid, &status, 0)) != pid)
+      if (TEMP_FAILURE_RETRY (__spawn_process_wait (pid, &status, 0)) != pid)
 	status = -1;
 #if defined(_LIBC_REENTRANT) && defined(SIGCANCEL)
       __libc_cleanup_region_end (0);
diff --git a/sysdeps/unix/sysv/linux/include/bits/spawn_ext.h b/sysdeps/unix/sysv/linux/include/bits/spawn_ext.h
new file mode 100644
index 0000000000..a7da319287
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/include/bits/spawn_ext.h
@@ -0,0 +1,11 @@ 
+#ifndef _SPAWN_EXT
+# define _SPAWN_EXT
+
+#include_next <bits/spawn_ext.h>
+
+#  ifndef _ISOMAC
+__typeof (pidfd_spawn) __pidfd_spawn;
+libc_hidden_proto (__pidfd_spawn);
+# endif
+
+#endif
diff --git a/sysdeps/unix/sysv/linux/not-errno.h b/sysdeps/unix/sysv/linux/not-errno.h
index dc484bd3f5..00992268eb 100644
--- a/sysdeps/unix/sysv/linux/not-errno.h
+++ b/sysdeps/unix/sysv/linux/not-errno.h
@@ -28,3 +28,14 @@  __kill_noerrno (pid_t pid, int sig)
     return INTERNAL_SYSCALL_ERRNO (res);
   return 0;
 }
+
+static inline int
+__pidfd_send_signal_noerrno (int pidfd, int sig, siginfo_t *info,
+			     unsigned int flags)
+{
+  int res;
+  res = INTERNAL_SYSCALL_CALL (pidfd_send_signal, pidfd, sig, info, flags);
+  if (INTERNAL_SYSCALL_ERROR_P (res))
+    return INTERNAL_SYSCALL_ERRNO (res);
+  return 0;
+}
diff --git a/sysdeps/unix/sysv/linux/pidfd_spawn.c b/sysdeps/unix/sysv/linux/pidfd_spawn.c
index a2e0a70017..fec1f29f89 100644
--- a/sysdeps/unix/sysv/linux/pidfd_spawn.c
+++ b/sysdeps/unix/sysv/linux/pidfd_spawn.c
@@ -20,11 +20,13 @@ 
 #include "spawn_int.h"
 
 int
-pidfd_spawn (int *pidfd, const char *path,
-	     const posix_spawn_file_actions_t *file_actions,
-	     const posix_spawnattr_t *attrp, char *const argv[],
-	     char *const envp[])
+__pidfd_spawn (int *pidfd, const char *path,
+	       const posix_spawn_file_actions_t *file_actions,
+	       const posix_spawnattr_t *attrp, char *const argv[],
+	       char *const envp[])
 {
   return __spawni (pidfd, path, file_actions, attrp, argv, envp,
 		   SPAWN_XFLAGS_RET_PIDFD);
 }
+libc_hidden_def (__pidfd_spawn)
+weak_alias (__pidfd_spawn, pidfd_spawn)
diff --git a/sysdeps/unix/sysv/linux/spawn_process.c b/sysdeps/unix/sysv/linux/spawn_process.c
new file mode 100644
index 0000000000..c525b9f8d7
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/spawn_process.c
@@ -0,0 +1,121 @@ 
+/* Internal implementation of __spawn_process*.  Linux implementation.
+   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 <spawn.h>
+#include <clone_internal.h>
+#include <not-errno.h>
+
+#pragma GCC optimize ("O0")
+
+int
+__spawn_process_create (process_create_id_t *procid,
+			const char *path,
+			const posix_spawn_file_actions_t *facts,
+			const posix_spawnattr_t *attr,
+			char *const argv[],
+			char *const envp[])
+{
+  int r;
+
+  if (__clone_pidfd_supported ())
+    {
+      int pidfd;
+      r = __pidfd_spawn (&pidfd, path, facts, attr, argv, envp);
+      if (r == 0)
+	{
+	  /* Both pidfd and pid_t do not allow negative values to describe a
+	     new process, so use the MSB to set the identifier is for
+	     pidfd.  */
+	  *procid = pidfd | 0x80000000;
+	  return 0;
+	}
+
+      /* Fallback to posix_spawn if file descriptor limits is reached.  */
+      if (r != EMFILE && r != ENFILE)
+	return r;
+    }
+
+  pid_t pid;
+  r = __posix_spawn (&pid, path, facts, attr, argv, envp);
+  if (r != 0)
+    return r;
+  *procid = pid;
+  return 0;
+}
+
+int
+__spawn_process_kill (process_create_id_t procid, int signo)
+{
+  return procid & 0x80000000
+    ? __pidfd_send_signal_noerrno (procid & INT_MAX, signo, NULL, 0)
+    : __kill_noerrno (procid, signo);
+}
+
+process_create_id_t
+__spawn_process_wait (process_create_id_t procid, int *wstatus, int options)
+{
+  bool use_pidfd = procid & 0x80000000;
+
+  siginfo_t info = { 0 };
+  int waitid_opts = WEXITED;
+  if (options & WNOHANG)
+    waitid_opts |= WNOHANG;
+  if (options & WUNTRACED)
+    waitid_opts |= WSTOPPED;
+  if (options & WCONTINUED)
+    waitid_opts |= WCONTINUED;
+
+  if (__waitid (use_pidfd ? P_PIDFD : P_PID,
+		use_pidfd ? procid & INT_MAX : procid,
+		&info,
+		waitid_opts) == -1)
+    return -1;
+
+  /* Handle successful WNOHANG but without a child state change.  */
+  if (info.si_pid == 0)
+    return 0;
+
+  if (wstatus != NULL)
+    {
+      int status = 0;
+      switch (info.si_code)
+	{
+	case CLD_EXITED:
+	  status = (info.si_status & 0xff) << 8;
+	  break;
+	case CLD_KILLED:
+	  status = info.si_status & 0x7f;
+	  break;
+	case CLD_DUMPED:
+	  status = (info.si_status & 0x7f) | __WCOREFLAG;
+	  break;
+	case CLD_STOPPED:
+	case CLD_TRAPPED:
+	  status = ((info.si_status & 0xff) << 8) | 0x7f;
+	  break;
+	case CLD_CONTINUED:
+	  status = 0xffff;
+	  break;
+        }
+      *wstatus = status;
+    }
+
+  /* With P_PIDFD, waitid populates info.si_pid with the actual Process ID of
+     the child.  */
+  return use_pidfd ? procid : info.si_pid;
+}