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

Adhemerval Zanella Netto <[email protected]> Thu, 30 Jul 2026 14:58:18 -0300
Newsgroups gmane.comp.lib.glibc.alpha
Organization Linaro
Message-ID <[email protected]>

On 29/04/26 17:00, Adhemerval Zanella wrote:
> 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.

Ping, should we considere this for 2.45?

> ---
>  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
> 
> 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;
> +}