gkdbus: Fix underflow and unreachable code bug
[platform/upstream/glib.git] / glib / gspawn.c
index 388b654..2f7465b 100644 (file)
@@ -4,20 +4,20 @@
  *  g_execvpe implementation based on GNU libc execvp:
  *   Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc.
  *
- * GLib 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 of the
- * License, or (at your option) any later version.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
  *
- * GLib is distributed in the hope that it will be useful,
+ * This 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.
+ *
+ * This 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 GLib; see the file COPYING.LIB.  If not, write
- * to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
- * Boston, MA 02111-1307, USA.
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
  */
 
 #include "config.h"
 #include <string.h>
 #include <stdlib.h>   /* for fdwalk */
 #include <dirent.h>
+#include <unistd.h>
+
+#ifdef HAVE_SPAWN_H
+#include <spawn.h>
+#endif /* HAVE_SPAWN_H */
+
+#ifdef HAVE_CRT_EXTERNS_H
+#include <crt_externs.h> /* for _NSGetEnviron */
+#endif
 
 #ifdef HAVE_SYS_SELECT_H
 #include <sys/select.h>
 #include <sys/resource.h>
 #endif /* HAVE_SYS_RESOURCE_H */
 
+#if defined(__linux__) || defined(__DragonFly__)
+#include <sys/syscall.h>  /* for syscall and SYS_getdents64 */
+#endif
+
 #include "gspawn.h"
+#include "gspawn-private.h"
+#include "gthread.h"
+#include "gtrace-private.h"
+#include "glib/gstdio.h"
 
 #include "genviron.h"
 #include "gmem.h"
 #include "gtestutils.h"
 #include "gutils.h"
 #include "glibintl.h"
+#include "glib-unix.h"
+
+#ifdef __APPLE__
+#include <libproc.h>
+#include <sys/proc_info.h>
+#endif
+
+#define INHERITS_OR_NULL_STDIN  (G_SPAWN_STDIN_FROM_DEV_NULL | G_SPAWN_CHILD_INHERITS_STDIN)
+#define INHERITS_OR_NULL_STDOUT (G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_CHILD_INHERITS_STDOUT)
+#define INHERITS_OR_NULL_STDERR (G_SPAWN_STDERR_TO_DEV_NULL | G_SPAWN_CHILD_INHERITS_STDERR)
+
+#define IS_STD_FILENO(_fd) ((_fd >= STDIN_FILENO) && (_fd <= STDERR_FILENO))
+#define IS_VALID_FILENO(_fd) (_fd >= 0)
+
+/* posix_spawn() is assumed the fastest way to spawn, but glibc's
+ * implementation was buggy before glibc 2.24, so avoid it on old versions.
+ */
+#ifdef HAVE_POSIX_SPAWN
+#ifdef __GLIBC__
+
+#if __GLIBC_PREREQ(2,24)
+#define POSIX_SPAWN_AVAILABLE
+#endif
+
+#else /* !__GLIBC__ */
+/* Assume that all non-glibc posix_spawn implementations are fine. */
+#define POSIX_SPAWN_AVAILABLE
+#endif /* __GLIBC__ */
+#endif /* HAVE_POSIX_SPAWN */
 
+#ifdef HAVE__NSGETENVIRON
+#define environ (*_NSGetEnviron())
+#else
+extern char **environ;
+#endif
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#else
+#define HAVE_O_CLOEXEC 1
+#endif
 
 /**
  * SECTION:spawn
  * @Short_description: process launching
  * @Title: Spawning Processes
+ *
+ * GLib supports spawning of processes with an API that is more
+ * convenient than the bare UNIX fork() and exec().
+ *
+ * The g_spawn family of functions has synchronous (g_spawn_sync())
+ * and asynchronous variants (g_spawn_async(), g_spawn_async_with_pipes()),
+ * as well as convenience variants that take a complete shell-like
+ * commandline (g_spawn_command_line_sync(), g_spawn_command_line_async()).
+ *
+ * See #GSubprocess in GIO for a higher-level API that provides
+ * stream interfaces for communication with child processes.
+ *
+ * An example of using g_spawn_async_with_pipes():
+ * |[<!-- language="C" -->
+ * const gchar * const argv[] = { "my-favourite-program", "--args", NULL };
+ * gint child_stdout, child_stderr;
+ * GPid child_pid;
+ * g_autoptr(GError) error = NULL;
+ *
+ * // Spawn child process.
+ * g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL,
+ *                           NULL, &child_pid, NULL, &child_stdout,
+ *                           &child_stderr, &error);
+ * if (error != NULL)
+ *   {
+ *     g_error ("Spawning child failed: %s", error->message);
+ *     return;
+ *   }
+ *
+ * // Add a child watch function which will be called when the child process
+ * // exits.
+ * g_child_watch_add (child_pid, child_watch_cb, NULL);
+ *
+ * // You could watch for output on @child_stdout and @child_stderr using
+ * // #GUnixInputStream or #GIOChannel here.
+ *
+ * static void
+ * child_watch_cb (GPid     pid,
+ *                 gint     status,
+ *                 gpointer user_data)
+ * {
+ *   g_message ("Child %" G_PID_FORMAT " exited %s", pid,
+ *              g_spawn_check_wait_status (status, NULL) ? "normally" : "abnormally");
+ *
+ *   // Free any resources associated with the child here, such as I/O channels
+ *   // on its stdout and stderr FDs. If you have no code to put in the
+ *   // child_watch_cb() callback, you can remove it and the g_child_watch_add()
+ *   // call, but you must also remove the G_SPAWN_DO_NOT_REAP_CHILD flag,
+ *   // otherwise the child process will stay around as a zombie until this
+ *   // process exits.
+ *
+ *   g_spawn_close_pid (pid);
+ * }
+ * ]|
  */
 
 
-
 static gint g_execute (const gchar  *file,
-                       gchar **argv,
-                       gchar **envp,
-                       gboolean search_path);
-
-static gboolean make_pipe            (gint                  p[2],
-                                      GError              **error);
-static gboolean fork_exec_with_pipes (gboolean              intermediate_child,
-                                      const gchar          *working_directory,
-                                      gchar               **argv,
-                                      gchar               **envp,
-                                      gboolean              close_descriptors,
-                                      gboolean              search_path,
-                                      gboolean              stdout_to_null,
-                                      gboolean              stderr_to_null,
-                                      gboolean              child_inherits_stdin,
-                                      gboolean              file_and_argv_zero,
-                                      GSpawnChildSetupFunc  child_setup,
-                                      gpointer              user_data,
-                                      GPid                 *child_pid,
-                                      gint                 *standard_input,
-                                      gint                 *standard_output,
-                                      gint                 *standard_error,
-                                      GError              **error);
-
-GQuark
-g_spawn_error_quark (void)
-{
-  return g_quark_from_static_string ("g-exec-error-quark");
-}
+                       gchar       **argv,
+                       gchar       **argv_buffer,
+                       gsize         argv_buffer_len,
+                       gchar       **envp,
+                       const gchar  *search_path,
+                       gchar        *search_path_buffer,
+                       gsize         search_path_buffer_len);
+
+static gboolean fork_exec (gboolean              intermediate_child,
+                           const gchar          *working_directory,
+                           const gchar * const  *argv,
+                           const gchar * const  *envp,
+                           gboolean              close_descriptors,
+                           gboolean              search_path,
+                           gboolean              search_path_from_envp,
+                           gboolean              stdout_to_null,
+                           gboolean              stderr_to_null,
+                           gboolean              child_inherits_stdin,
+                           gboolean              file_and_argv_zero,
+                           gboolean              cloexec_pipes,
+                           GSpawnChildSetupFunc  child_setup,
+                           gpointer              user_data,
+                           GPid                 *child_pid,
+                           gint                 *stdin_pipe_out,
+                           gint                 *stdout_pipe_out,
+                           gint                 *stderr_pipe_out,
+                           gint                  stdin_fd,
+                           gint                  stdout_fd,
+                           gint                  stderr_fd,
+                           const gint           *source_fds,
+                           const gint           *target_fds,
+                           gsize                 n_fds,
+                           GError              **error);
+
+G_DEFINE_QUARK (g-exec-error-quark, g_spawn_error)
+G_DEFINE_QUARK (g-spawn-exit-error-quark, g_spawn_exit_error)
 
 /**
  * g_spawn_async:
- * @working_directory: (allow-none): child's current working directory, or %NULL to inherit parent's
- * @argv: (array zero-terminated=1): child's argument vector
- * @envp: (array zero-terminated=1) (allow-none): child's environment, or %NULL to inherit parent's
+ * @working_directory: (type filename) (nullable): child's current working
+ *     directory, or %NULL to inherit parent's
+ * @argv: (array zero-terminated=1) (element-type filename):
+ *     child's argument vector
+ * @envp: (array zero-terminated=1) (element-type filename) (nullable):
+ *     child's environment, or %NULL to inherit parent's
  * @flags: flags from #GSpawnFlags
- * @child_setup: (scope async) (allow-none): function to run in the child just before exec()
- * @user_data: (closure): user data for @child_setup
- * @child_pid: (out) (allow-none): return location for child process reference, or %NULL
+ * @child_setup: (scope async) (closure user_data) (nullable): function to run
+ *     in the child just before `exec()`
+ * @user_data: user data for @child_setup
+ * @child_pid: (out) (optional): return location for child process reference, or %NULL
  * @error: return location for error
+ *
+ * Executes a child program asynchronously.
  * 
  * See g_spawn_async_with_pipes() for a full description; this function
  * simply calls the g_spawn_async_with_pipes() without any pipes.
@@ -109,19 +232,16 @@ g_spawn_error_quark (void)
  * You should call g_spawn_close_pid() on the returned child process
  * reference when you don't need it any more.
  * 
- * <note><para>
- * If you are writing a GTK+ application, and the program you 
- * are spawning is a graphical application, too, then you may
- * want to use gdk_spawn_on_screen() instead to ensure that
- * the spawned program opens its windows on the right screen.
- * </para></note>
- *
- * <note><para> Note that the returned @child_pid on Windows is a
- * handle to the child process and not its identifier. Process handles
- * and process identifiers are different concepts on Windows.
- * </para></note>
- *
- * Return value: %TRUE on success, %FALSE if error is set
+ * If you are writing a GTK application, and the program you are spawning is a
+ * graphical application too, then to ensure that the spawned program opens its
+ * windows on the right screen, you may want to use #GdkAppLaunchContext,
+ * #GAppLaunchContext, or set the %DISPLAY environment variable.
+ *
+ * Note that the returned @child_pid on Windows is a handle to the child
+ * process and not its identifier. Process handles and process identifiers
+ * are different concepts on Windows.
+ *
+ * Returns: %TRUE on success, %FALSE if error is set
  **/
 gboolean
 g_spawn_async (const gchar          *working_directory,
@@ -133,8 +253,6 @@ g_spawn_async (const gchar          *working_directory,
                GPid                 *child_pid,
                GError              **error)
 {
-  g_return_val_if_fail (argv != NULL, FALSE);
-  
   return g_spawn_async_with_pipes (working_directory,
                                    argv, envp,
                                    flags,
@@ -148,24 +266,18 @@ g_spawn_async (const gchar          *working_directory,
 /* Avoids a danger in threaded situations (calling close()
  * on a file descriptor twice, and another thread has
  * re-opened it since the first close)
+ *
+ * This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)).
  */
-static gint
+static void
 close_and_invalidate (gint *fd)
 {
-  gint ret;
-
   if (*fd < 0)
-    return -1;
-  else
-    {
-    again:
-      ret = close (*fd);
-      if (ret == -1 && errno == EINTR)
-       goto again;
-      *fd = -1;
-    }
+    return;
 
-  return ret;
+  g_close (*fd, NULL);
+  *fd = -1;
 }
 
 /* Some versions of OS X define READ_OK in public headers */
@@ -214,36 +326,47 @@ read_data (GString *str,
 
 /**
  * g_spawn_sync:
- * @working_directory: (allow-none): child's current working directory, or %NULL to inherit parent's
- * @argv: (array zero-terminated=1): child's argument vector
- * @envp: (array zero-terminated=1) (allow-none): child's environment, or %NULL to inherit parent's
+ * @working_directory: (type filename) (nullable): child's current working
+ *     directory, or %NULL to inherit parent's
+ * @argv: (array zero-terminated=1) (element-type filename):
+ *     child's argument vector, which must be non-empty and %NULL-terminated
+ * @envp: (array zero-terminated=1) (element-type filename) (nullable):
+ *     child's environment, or %NULL to inherit parent's
  * @flags: flags from #GSpawnFlags
- * @child_setup: (scope async) (allow-none): function to run in the child just before exec()
- * @user_data: (closure): user data for @child_setup
- * @standard_output: (out) (array zero-terminated=1) (element-type guint8) (allow-none): return location for child output, or %NULL
- * @standard_error: (out) (array zero-terminated=1) (element-type guint8) (allow-none): return location for child error messages, or %NULL
- * @exit_status: (out) (allow-none): return location for child exit status, as returned by waitpid(), or %NULL
+ * @child_setup: (scope call) (closure user_data) (nullable): function to run
+ *     in the child just before `exec()`
+ * @user_data: user data for @child_setup
+ * @standard_output: (out) (array zero-terminated=1) (element-type guint8) (optional): return location for child output, or %NULL
+ * @standard_error: (out) (array zero-terminated=1) (element-type guint8) (optional): return location for child error messages, or %NULL
+ * @wait_status: (out) (optional): return location for child wait status, as returned by waitpid(), or %NULL
  * @error: return location for error, or %NULL
  *
  * Executes a child synchronously (waits for the child to exit before returning).
+ *
  * All output from the child is stored in @standard_output and @standard_error,
  * if those parameters are non-%NULL. Note that you must set the  
  * %G_SPAWN_STDOUT_TO_DEV_NULL and %G_SPAWN_STDERR_TO_DEV_NULL flags when
  * passing %NULL for @standard_output and @standard_error.
- * If @exit_status is non-%NULL, the exit status of the child is stored
- * there as it would be returned by waitpid(); standard UNIX macros such 
- * as WIFEXITED() and WEXITSTATUS() must be used to evaluate the exit status.
- * Note that this function call waitpid() even if @exit_status is %NULL, and
- * does not accept the %G_SPAWN_DO_NOT_REAP_CHILD flag.
- * If an error occurs, no data is returned in @standard_output, 
- * @standard_error, or @exit_status. 
+ *
+ * If @wait_status is non-%NULL, the platform-specific status of
+ * the child is stored there; see the documentation of
+ * g_spawn_check_wait_status() for how to use and interpret this.
+ * On Unix platforms, note that it is usually not equal
+ * to the integer passed to `exit()` or returned from `main()`.
+ *
+ * Note that it is invalid to pass %G_SPAWN_DO_NOT_REAP_CHILD in
+ * @flags, and on POSIX platforms, the same restrictions as for
+ * g_child_watch_source_new() apply.
+ *
+ * If an error occurs, no data is returned in @standard_output,
+ * @standard_error, or @wait_status.
  *
  * This function calls g_spawn_async_with_pipes() internally; see that
  * function for full details on the other parameters and details on
  * how these functions work on Windows.
  * 
- * Return value: %TRUE on success, %FALSE if an error was set.
- **/
+ * Returns: %TRUE on success, %FALSE if an error was set
+ */
 gboolean
 g_spawn_sync (const gchar          *working_directory,
               gchar               **argv,
@@ -253,13 +376,12 @@ g_spawn_sync (const gchar          *working_directory,
               gpointer              user_data,
               gchar               **standard_output,
               gchar               **standard_error,
-              gint                 *exit_status,
+              gint                 *wait_status,
               GError              **error)     
 {
   gint outpipe = -1;
   gint errpipe = -1;
   GPid pid;
-  fd_set fds;
   gint ret;
   GString *outstr = NULL;
   GString *errstr = NULL;
@@ -267,6 +389,7 @@ g_spawn_sync (const gchar          *working_directory,
   gint status;
   
   g_return_val_if_fail (argv != NULL, FALSE);
+  g_return_val_if_fail (argv[0] != NULL, FALSE);
   g_return_val_if_fail (!(flags & G_SPAWN_DO_NOT_REAP_CHILD), FALSE);
   g_return_val_if_fail (standard_output == NULL ||
                         !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE);
@@ -282,23 +405,27 @@ g_spawn_sync (const gchar          *working_directory,
   if (standard_error)
     *standard_error = NULL;
   
-  if (!fork_exec_with_pipes (FALSE,
-                             working_directory,
-                             argv,
-                             envp,
-                             !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
-                             (flags & G_SPAWN_SEARCH_PATH) != 0,
-                             (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0,
-                             (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0,
-                             (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0,
-                             (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
-                             child_setup,
-                             user_data,
-                             &pid,
-                             NULL,
-                             standard_output ? &outpipe : NULL,
-                             standard_error ? &errpipe : NULL,
-                             error))
+  if (!fork_exec (FALSE,
+                  working_directory,
+                  (const gchar * const *) argv,
+                  (const gchar * const *) envp,
+                  !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
+                  (flags & G_SPAWN_SEARCH_PATH) != 0,
+                  (flags & G_SPAWN_SEARCH_PATH_FROM_ENVP) != 0,
+                  (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0,
+                  (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0,
+                  (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0,
+                  (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
+                  (flags & G_SPAWN_CLOEXEC_PIPES) != 0,
+                  child_setup,
+                  user_data,
+                  &pid,
+                  NULL,
+                  standard_output ? &outpipe : NULL,
+                  standard_error ? &errpipe : NULL,
+                  -1, -1, -1,
+                  NULL, NULL, 0,
+                  error))
     return FALSE;
 
   /* Read data from child. */
@@ -320,18 +447,16 @@ g_spawn_sync (const gchar          *working_directory,
          (outpipe >= 0 ||
           errpipe >= 0))
     {
-      ret = 0;
-          
-      FD_ZERO (&fds);
-      if (outpipe >= 0)
-        FD_SET (outpipe, &fds);
-      if (errpipe >= 0)
-        FD_SET (errpipe, &fds);
-          
-      ret = select (MAX (outpipe, errpipe) + 1,
-                    &fds,
-                    NULL, NULL,
-                    NULL /* no timeout */);
+      /* Any negative FD in the array is ignored, so we can use a fixed length.
+       * We can use UNIX FDs here without worrying about Windows HANDLEs because
+       * the Windows implementation is entirely in gspawn-win32.c. */
+      GPollFD fds[] =
+        {
+          { outpipe, G_IO_IN | G_IO_HUP | G_IO_ERR, 0 },
+          { errpipe, G_IO_IN | G_IO_HUP | G_IO_ERR, 0 },
+        };
+
+      ret = g_poll (fds, G_N_ELEMENTS (fds), -1  /* no timeout */);
 
       if (ret < 0)
         {
@@ -345,13 +470,13 @@ g_spawn_sync (const gchar          *working_directory,
           g_set_error (error,
                        G_SPAWN_ERROR,
                        G_SPAWN_ERROR_READ,
-                       _("Unexpected error in select() reading data from a child process (%s)"),
+                       _("Unexpected error in reading data from a child process (%s)"),
                        g_strerror (errsv));
               
           break;
         }
 
-      if (outpipe >= 0 && FD_ISSET (outpipe, &fds))
+      if (outpipe >= 0 && fds[0].revents != 0)
         {
           switch (read_data (outstr, outpipe, error))
             {
@@ -370,7 +495,7 @@ g_spawn_sync (const gchar          *working_directory,
             break;
         }
 
-      if (errpipe >= 0 && FD_ISSET (errpipe, &fds))
+      if (errpipe >= 0 && fds[1].revents != 0)
         {
           switch (read_data (errstr, errpipe, error))
             {
@@ -410,13 +535,13 @@ g_spawn_sync (const gchar          *working_directory,
         goto again;
       else if (errno == ECHILD)
         {
-          if (exit_status)
+          if (wait_status)
             {
-              g_warning ("In call to g_spawn_sync(), exit status of a child process was requested but SIGCHLD action was set to SIG_IGN and ECHILD was received by waitpid(), so exit status can't be returned. This is a bug in the program calling g_spawn_sync(); either don't request the exit status, or don't set the SIGCHLD action.");
+              g_warning ("In call to g_spawn_sync(), wait status of a child process was requested but ECHILD was received by waitpid(). See the documentation of g_child_watch_source_new() for possible causes.");
             }
           else
             {
-              /* We don't need the exit status. */
+              /* We don't need the wait status. */
             }
         }
       else
@@ -447,8 +572,8 @@ g_spawn_sync (const gchar          *working_directory,
     }
   else
     {
-      if (exit_status)
-        *exit_status = status;
+      if (wait_status)
+        *wait_status = status;
       
       if (standard_output)        
         *standard_output = g_string_free (outstr, FALSE);
@@ -462,233 +587,443 @@ g_spawn_sync (const gchar          *working_directory,
 
 /**
  * g_spawn_async_with_pipes:
- * @working_directory: (allow-none): child's current working directory, or %NULL to inherit parent's, in the GLib file name encoding
- * @argv: (array zero-terminated=1): child's argument vector, in the GLib file name encoding
- * @envp: (array zero-terminated=1) (allow-none): child's environment, or %NULL to inherit parent's, in the GLib file name encoding
+ * @working_directory: (type filename) (nullable): child's current working
+ *     directory, or %NULL to inherit parent's, in the GLib file name encoding
+ * @argv: (array zero-terminated=1) (element-type filename): child's argument
+ *     vector, in the GLib file name encoding; it must be non-empty and %NULL-terminated
+ * @envp: (array zero-terminated=1) (element-type filename) (nullable):
+ *     child's environment, or %NULL to inherit parent's, in the GLib file
+ *     name encoding
+ * @flags: flags from #GSpawnFlags
+ * @child_setup: (scope async) (closure user_data) (nullable): function to run
+ *     in the child just before `exec()`
+ * @user_data: user data for @child_setup
+ * @child_pid: (out) (optional): return location for child process ID, or %NULL
+ * @standard_input: (out) (optional): return location for file descriptor to write to child's stdin, or %NULL
+ * @standard_output: (out) (optional): return location for file descriptor to read child's stdout, or %NULL
+ * @standard_error: (out) (optional): return location for file descriptor to read child's stderr, or %NULL
+ * @error: return location for error
+ *
+ * Identical to g_spawn_async_with_pipes_and_fds() but with `n_fds` set to zero,
+ * so no FD assignments are used.
+ *
+ * Returns: %TRUE on success, %FALSE if an error was set
+ */
+gboolean
+g_spawn_async_with_pipes (const gchar          *working_directory,
+                          gchar               **argv,
+                          gchar               **envp,
+                          GSpawnFlags           flags,
+                          GSpawnChildSetupFunc  child_setup,
+                          gpointer              user_data,
+                          GPid                 *child_pid,
+                          gint                 *standard_input,
+                          gint                 *standard_output,
+                          gint                 *standard_error,
+                          GError              **error)
+{
+  return g_spawn_async_with_pipes_and_fds (working_directory,
+                                           (const gchar * const *) argv,
+                                           (const gchar * const *) envp,
+                                           flags,
+                                           child_setup, user_data,
+                                           -1, -1, -1,
+                                           NULL, NULL, 0,
+                                           child_pid,
+                                           standard_input,
+                                           standard_output,
+                                           standard_error,
+                                           error);
+}
+
+/**
+ * g_spawn_async_with_pipes_and_fds:
+ * @working_directory: (type filename) (nullable): child's current working
+ *     directory, or %NULL to inherit parent's, in the GLib file name encoding
+ * @argv: (array zero-terminated=1) (element-type filename): child's argument
+ *     vector, in the GLib file name encoding; it must be non-empty and %NULL-terminated
+ * @envp: (array zero-terminated=1) (element-type filename) (nullable):
+ *     child's environment, or %NULL to inherit parent's, in the GLib file
+ *     name encoding
  * @flags: flags from #GSpawnFlags
- * @child_setup: (scope async) (allow-none): function to run in the child just before exec()
- * @user_data: (closure): user data for @child_setup
- * @child_pid: (out) (allow-none): return location for child process ID, or %NULL
- * @standard_input: (out) (allow-none): return location for file descriptor to write to child's stdin, or %NULL
- * @standard_output: (out) (allow-none): return location for file descriptor to read child's stdout, or %NULL
- * @standard_error: (out) (allow-none): return location for file descriptor to read child's stderr, or %NULL
+ * @child_setup: (scope async) (closure user_data) (nullable): function to run
+ *     in the child just before `exec()`
+ * @user_data: user data for @child_setup
+ * @stdin_fd: file descriptor to use for child's stdin, or `-1`
+ * @stdout_fd: file descriptor to use for child's stdout, or `-1`
+ * @stderr_fd: file descriptor to use for child's stderr, or `-1`
+ * @source_fds: (array length=n_fds) (nullable): array of FDs from the parent
+ *    process to make available in the child process
+ * @target_fds: (array length=n_fds) (nullable): array of FDs to remap
+ *    @source_fds to in the child process
+ * @n_fds: number of FDs in @source_fds and @target_fds
+ * @child_pid_out: (out) (optional): return location for child process ID, or %NULL
+ * @stdin_pipe_out: (out) (optional): return location for file descriptor to write to child's stdin, or %NULL
+ * @stdout_pipe_out: (out) (optional): return location for file descriptor to read child's stdout, or %NULL
+ * @stderr_pipe_out: (out) (optional): return location for file descriptor to read child's stderr, or %NULL
  * @error: return location for error
  *
  * Executes a child program asynchronously (your program will not
- * block waiting for the child to exit). The child program is
- * specified by the only argument that must be provided, @argv. @argv
- * should be a %NULL-terminated array of strings, to be passed as the
- * argument vector for the child. The first string in @argv is of
- * course the name of the program to execute. By default, the name of
- * the program must be a full path; the <envar>PATH</envar> shell variable 
- * will only be searched if you pass the %G_SPAWN_SEARCH_PATH flag.
- * If the program name is not a full path and %G_SPAWN_SEARCH_PATH flag is not
- * used, then the program will be run from the current directory (or
- * @working_directory, if specified); this might be unexpected or even
+ * block waiting for the child to exit).
+ *
+ * The child program is specified by the only argument that must be
+ * provided, @argv. @argv should be a %NULL-terminated array of strings,
+ * to be passed as the argument vector for the child. The first string
+ * in @argv is of course the name of the program to execute. By default,
+ * the name of the program must be a full path. If @flags contains the
+ * %G_SPAWN_SEARCH_PATH flag, the `PATH` environment variable is used to
+ * search for the executable. If @flags contains the
+ * %G_SPAWN_SEARCH_PATH_FROM_ENVP flag, the `PATH` variable from @envp
+ * is used to search for the executable. If both the
+ * %G_SPAWN_SEARCH_PATH and %G_SPAWN_SEARCH_PATH_FROM_ENVP flags are
+ * set, the `PATH` variable from @envp takes precedence over the
+ * environment variable.
+ *
+ * If the program name is not a full path and %G_SPAWN_SEARCH_PATH flag
+ * is not used, then the program will be run from the current directory
+ * (or @working_directory, if specified); this might be unexpected or even
  * dangerous in some cases when the current directory is world-writable.
  *
  * On Windows, note that all the string or string vector arguments to
- * this function and the other g_spawn*() functions are in UTF-8, the
+ * this function and the other `g_spawn*()` functions are in UTF-8, the
  * GLib file name encoding. Unicode characters that are not part of
  * the system codepage passed in these arguments will be correctly
  * available in the spawned program only if it uses wide character API
  * to retrieve its command line. For C programs built with Microsoft's
- * tools it is enough to make the program have a wmain() instead of
- * main(). wmain() has a wide character argument vector as parameter.
- *
- * At least currently, mingw doesn't support wmain(), so if you use
- * mingw to develop the spawned program, it will have to call the
- * undocumented function __wgetmainargs() to get the wide character
- * argument vector and environment. See gspawn-win32-helper.c in the
- * GLib sources or init.c in the mingw runtime sources for a prototype
- * for that function. Alternatively, you can retrieve the Win32 system
- * level wide character command line passed to the spawned program
- * using the GetCommandLineW() function.
- *
- * On Windows the low-level child process creation API
- * <function>CreateProcess()</function> doesn't use argument vectors,
- * but a command line. The C runtime library's
- * <function>spawn*()</function> family of functions (which
- * g_spawn_async_with_pipes() eventually calls) paste the argument
- * vector elements together into a command line, and the C runtime startup code
- * does a corresponding reconstruction of an argument vector from the
- * command line, to be passed to main(). Complications arise when you have
- * argument vector elements that contain spaces of double quotes. The
- * <function>spawn*()</function> functions don't do any quoting or
- * escaping, but on the other hand the startup code does do unquoting
- * and unescaping in order to enable receiving arguments with embedded
- * spaces or double quotes. To work around this asymmetry,
- * g_spawn_async_with_pipes() will do quoting and escaping on argument
- * vector elements that need it before calling the C runtime
- * spawn() function.
+ * tools it is enough to make the program have a `wmain()` instead of
+ * `main()`. `wmain()` has a wide character argument vector as parameter.
+ *
+ * At least currently, mingw doesn't support `wmain()`, so if you use
+ * mingw to develop the spawned program, it should call
+ * g_win32_get_command_line() to get arguments in UTF-8.
+ *
+ * On Windows the low-level child process creation API `CreateProcess()`
+ * doesn't use argument vectors, but a command line. The C runtime
+ * library's `spawn*()` family of functions (which g_spawn_async_with_pipes()
+ * eventually calls) paste the argument vector elements together into
+ * a command line, and the C runtime startup code does a corresponding
+ * reconstruction of an argument vector from the command line, to be
+ * passed to `main()`. Complications arise when you have argument vector
+ * elements that contain spaces or double quotes. The `spawn*()` functions
+ * don't do any quoting or escaping, but on the other hand the startup
+ * code does do unquoting and unescaping in order to enable receiving
+ * arguments with embedded spaces or double quotes. To work around this
+ * asymmetry, g_spawn_async_with_pipes() will do quoting and escaping on
+ * argument vector elements that need it before calling the C runtime
+ * `spawn()` function.
  *
  * The returned @child_pid on Windows is a handle to the child
  * process, not its identifier. Process handles and process
  * identifiers are different concepts on Windows.
  *
  * @envp is a %NULL-terminated array of strings, where each string
- * has the form <literal>KEY=VALUE</literal>. This will become
- * the child's environment. If @envp is %NULL, the child inherits its
- * parent's environment.
+ * has the form `KEY=VALUE`. This will become the child's environment.
+ * If @envp is %NULL, the child inherits its parent's environment.
  *
  * @flags should be the bitwise OR of any flags you want to affect the
  * function's behaviour. The %G_SPAWN_DO_NOT_REAP_CHILD means that the
- * child will not automatically be reaped; you must use a child watch to
- * be notified about the death of the child process. Eventually you must
- * call g_spawn_close_pid() on the @child_pid, in order to free
- * resources which may be associated with the child process. (On Unix,
+ * child will not automatically be reaped; you must use a child watch
+ * (g_child_watch_add()) to be notified about the death of the child process,
+ * otherwise it will stay around as a zombie process until this process exits.
+ * Eventually you must call g_spawn_close_pid() on the @child_pid, in order to
+ * free resources which may be associated with the child process. (On Unix,
  * using a child watch is equivalent to calling waitpid() or handling
- * the <literal>SIGCHLD</literal> signal manually. On Windows, calling g_spawn_close_pid()
- * is equivalent to calling CloseHandle() on the process handle returned
- * in @child_pid).  See g_child_watch_add().
- *
- * %G_SPAWN_LEAVE_DESCRIPTORS_OPEN means that the parent's open file
- * descriptors will be inherited by the child; otherwise all
- * descriptors except stdin/stdout/stderr will be closed before
- * calling exec() in the child. %G_SPAWN_SEARCH_PATH 
- * means that <literal>argv[0]</literal> need not be an absolute path, it
- * will be looked for in the user's <envar>PATH</envar>. 
- * %G_SPAWN_STDOUT_TO_DEV_NULL means that the child's standard output will 
- * be discarded, instead of going to the same location as the parent's 
- * standard output. If you use this flag, @standard_output must be %NULL.
- * %G_SPAWN_STDERR_TO_DEV_NULL means that the child's standard error
- * will be discarded, instead of going to the same location as the parent's
- * standard error. If you use this flag, @standard_error must be %NULL.
+ * the `SIGCHLD` signal manually. On Windows, calling g_spawn_close_pid()
+ * is equivalent to calling `CloseHandle()` on the process handle returned
+ * in @child_pid). See g_child_watch_add().
+ *
+ * Open UNIX file descriptors marked as `FD_CLOEXEC` will be automatically
+ * closed in the child process. %G_SPAWN_LEAVE_DESCRIPTORS_OPEN means that
+ * other open file descriptors will be inherited by the child; otherwise all
+ * descriptors except stdin/stdout/stderr will be closed before calling `exec()`
+ * in the child. %G_SPAWN_SEARCH_PATH means that @argv[0] need not be an
+ * absolute path, it will be looked for in the `PATH` environment
+ * variable. %G_SPAWN_SEARCH_PATH_FROM_ENVP means need not be an
+ * absolute path, it will be looked for in the `PATH` variable from
+ * @envp. If both %G_SPAWN_SEARCH_PATH and %G_SPAWN_SEARCH_PATH_FROM_ENVP
+ * are used, the value from @envp takes precedence over the environment.
+ *
  * %G_SPAWN_CHILD_INHERITS_STDIN means that the child will inherit the parent's
  * standard input (by default, the child's standard input is attached to
- * /dev/null). If you use this flag, @standard_input must be %NULL.
+ * `/dev/null`). %G_SPAWN_STDIN_FROM_DEV_NULL explicitly imposes the default
+ * behavior. Both flags cannot be enabled at the same time and, in both cases,
+ * the @stdin_pipe_out argument is ignored.
+ *
+ * %G_SPAWN_STDOUT_TO_DEV_NULL means that the child's standard output
+ * will be discarded (by default, it goes to the same location as the parent's
+ * standard output). %G_SPAWN_CHILD_INHERITS_STDOUT explicitly imposes the
+ * default behavior. Both flags cannot be enabled at the same time and, in
+ * both cases, the @stdout_pipe_out argument is ignored.
+ *
+ * %G_SPAWN_STDERR_TO_DEV_NULL means that the child's standard error
+ * will be discarded (by default, it goes to the same location as the parent's
+ * standard error). %G_SPAWN_CHILD_INHERITS_STDERR explicitly imposes the
+ * default behavior. Both flags cannot be enabled at the same time and, in
+ * both cases, the @stderr_pipe_out argument is ignored.
+ *
+ * It is valid to pass the same FD in multiple parameters (e.g. you can pass
+ * a single FD for both @stdout_fd and @stderr_fd, and include it in
+ * @source_fds too).
+ *
+ * @source_fds and @target_fds allow zero or more FDs from this process to be
+ * remapped to different FDs in the spawned process. If @n_fds is greater than
+ * zero, @source_fds and @target_fds must both be non-%NULL and the same length.
+ * Each FD in @source_fds is remapped to the FD number at the same index in
+ * @target_fds. The source and target FD may be equal to simply propagate an FD
+ * to the spawned process. FD remappings are processed after standard FDs, so
+ * any target FDs which equal @stdin_fd, @stdout_fd or @stderr_fd will overwrite
+ * them in the spawned process.
+ *
+ * @source_fds is supported on Windows since 2.72.
+ *
  * %G_SPAWN_FILE_AND_ARGV_ZERO means that the first element of @argv is
- * the file to execute, while the remaining elements are the
- * actual argument vector to pass to the file. Normally
- * g_spawn_async_with_pipes() uses @argv[0] as the file to execute, and
- * passes all of @argv to the child.
+ * the file to execute, while the remaining elements are the actual
+ * argument vector to pass to the file. Normally g_spawn_async_with_pipes()
+ * uses @argv[0] as the file to execute, and passes all of @argv to the child.
  *
  * @child_setup and @user_data are a function and user data. On POSIX
  * platforms, the function is called in the child after GLib has
  * performed all the setup it plans to perform (including creating
- * pipes, closing file descriptors, etc.) but before calling
- * exec(). That is, @child_setup is called just
- * before calling exec() in the child. Obviously
- * actions taken in this function will only affect the child, not the
- * parent.
- *
- * On Windows, there is no separate fork() and exec()
- * functionality. Child processes are created and run with a single
- * API call, CreateProcess(). There is no sensible thing @child_setup
+ * pipes, closing file descriptors, etc.) but before calling `exec()`.
+ * That is, @child_setup is called just before calling `exec()` in the
+ * child. Obviously actions taken in this function will only affect
+ * the child, not the parent.
+ *
+ * On Windows, there is no separate `fork()` and `exec()` functionality.
+ * Child processes are created and run with a single API call,
+ * `CreateProcess()`. There is no sensible thing @child_setup
  * could be used for on Windows so it is ignored and not called.
  *
  * If non-%NULL, @child_pid will on Unix be filled with the child's
- * process ID. You can use the process ID to send signals to the
- * child, or to use g_child_watch_add() (or waitpid()) if you specified the
+ * process ID. You can use the process ID to send signals to the child,
+ * or to use g_child_watch_add() (or `waitpid()`) if you specified the
  * %G_SPAWN_DO_NOT_REAP_CHILD flag. On Windows, @child_pid will be
  * filled with a handle to the child process only if you specified the
  * %G_SPAWN_DO_NOT_REAP_CHILD flag. You can then access the child
  * process using the Win32 API, for example wait for its termination
- * with the <function>WaitFor*()</function> functions, or examine its
- * exit code with GetExitCodeProcess(). You should close the handle 
- * with CloseHandle() or g_spawn_close_pid() when you no longer need it.
+ * with the `WaitFor*()` functions, or examine its exit code with
+ * `GetExitCodeProcess()`. You should close the handle with `CloseHandle()`
+ * or g_spawn_close_pid() when you no longer need it.
  *
- * If non-%NULL, the @standard_input, @standard_output, @standard_error
+ * If non-%NULL, the @stdin_pipe_out, @stdout_pipe_out, @stderr_pipe_out
  * locations will be filled with file descriptors for writing to the child's
  * standard input or reading from its standard output or standard error.
  * The caller of g_spawn_async_with_pipes() must close these file descriptors
- * when they are no longer in use. If these parameters are %NULL, the corresponding
- * pipe won't be created.
+ * when they are no longer in use. If these parameters are %NULL, the
+ * corresponding pipe won't be created.
  *
- * If @standard_input is NULL, the child's standard input is attached to 
- * /dev/null unless %G_SPAWN_CHILD_INHERITS_STDIN is set.
+ * If @stdin_pipe_out is %NULL, the child's standard input is attached to
+ * `/dev/null` unless %G_SPAWN_CHILD_INHERITS_STDIN is set.
  *
- * If @standard_error is NULL, the child's standard error goes to the same 
- * location as the parent's standard error unless %G_SPAWN_STDERR_TO_DEV_NULL 
+ * If @stderr_pipe_out is NULL, the child's standard error goes to the same
+ * location as the parent's standard error unless %G_SPAWN_STDERR_TO_DEV_NULL
  * is set.
  *
- * If @standard_output is NULL, the child's standard output goes to the same 
- * location as the parent's standard output unless %G_SPAWN_STDOUT_TO_DEV_NULL 
+ * If @stdout_pipe_out is NULL, the child's standard output goes to the same
+ * location as the parent's standard output unless %G_SPAWN_STDOUT_TO_DEV_NULL
  * is set.
  *
  * @error can be %NULL to ignore errors, or non-%NULL to report errors.
- * If an error is set, the function returns %FALSE. Errors
- * are reported even if they occur in the child (for example if the
- * executable in <literal>argv[0]</literal> is not found). Typically
- * the <literal>message</literal> field of returned errors should be displayed
- * to users. Possible errors are those from the #G_SPAWN_ERROR domain.
+ * If an error is set, the function returns %FALSE. Errors are reported
+ * even if they occur in the child (for example if the executable in
+ * `@argv[0]` is not found). Typically the `message` field of returned
+ * errors should be displayed to users. Possible errors are those from
+ * the %G_SPAWN_ERROR domain.
  *
- * If an error occurs, @child_pid, @standard_input, @standard_output,
- * and @standard_error will not be filled with valid values.
+ * If an error occurs, @child_pid, @stdin_pipe_out, @stdout_pipe_out,
+ * and @stderr_pipe_out will not be filled with valid values.
  *
  * If @child_pid is not %NULL and an error does not occur then the returned
  * process reference must be closed using g_spawn_close_pid().
  *
- * <note><para>
- * If you are writing a GTK+ application, and the program you 
- * are spawning is a graphical application, too, then you may
- * want to use gdk_spawn_on_screen_with_pipes() instead to ensure that
- * the spawned program opens its windows on the right screen.
- * </para></note>
- * 
- * Return value: %TRUE on success, %FALSE if an error was set
- **/
+ * On modern UNIX platforms, GLib can use an efficient process launching
+ * codepath driven internally by `posix_spawn()`. This has the advantage of
+ * avoiding the fork-time performance costs of cloning the parent process
+ * address space, and avoiding associated memory overcommit checks that are
+ * not relevant in the context of immediately executing a distinct process.
+ * This optimized codepath will be used provided that the following conditions
+ * are met:
+ *
+ * 1. %G_SPAWN_DO_NOT_REAP_CHILD is set
+ * 2. %G_SPAWN_LEAVE_DESCRIPTORS_OPEN is set
+ * 3. %G_SPAWN_SEARCH_PATH_FROM_ENVP is not set
+ * 4. @working_directory is %NULL
+ * 5. @child_setup is %NULL
+ * 6. The program is of a recognised binary format, or has a shebang.
+ *    Otherwise, GLib will have to execute the program through the
+ *    shell, which is not done using the optimized codepath.
+ *
+ * If you are writing a GTK application, and the program you are spawning is a
+ * graphical application too, then to ensure that the spawned program opens its
+ * windows on the right screen, you may want to use #GdkAppLaunchContext,
+ * #GAppLaunchContext, or set the `DISPLAY` environment variable.
+ *
+ * Returns: %TRUE on success, %FALSE if an error was set
+ *
+ * Since: 2.68
+ */
 gboolean
-g_spawn_async_with_pipes (const gchar          *working_directory,
-                          gchar               **argv,
-                          gchar               **envp,
-                          GSpawnFlags           flags,
-                          GSpawnChildSetupFunc  child_setup,
-                          gpointer              user_data,
-                          GPid                 *child_pid,
-                          gint                 *standard_input,
-                          gint                 *standard_output,
-                          gint                 *standard_error,
-                          GError              **error)
+g_spawn_async_with_pipes_and_fds (const gchar           *working_directory,
+                                  const gchar * const   *argv,
+                                  const gchar * const   *envp,
+                                  GSpawnFlags            flags,
+                                  GSpawnChildSetupFunc   child_setup,
+                                  gpointer               user_data,
+                                  gint                   stdin_fd,
+                                  gint                   stdout_fd,
+                                  gint                   stderr_fd,
+                                  const gint            *source_fds,
+                                  const gint            *target_fds,
+                                  gsize                  n_fds,
+                                  GPid                  *child_pid_out,
+                                  gint                  *stdin_pipe_out,
+                                  gint                  *stdout_pipe_out,
+                                  gint                  *stderr_pipe_out,
+                                  GError               **error)
 {
   g_return_val_if_fail (argv != NULL, FALSE);
-  g_return_val_if_fail (standard_output == NULL ||
+  g_return_val_if_fail (argv[0] != NULL, FALSE);
+  /* can’t both inherit and set pipes to /dev/null */
+  g_return_val_if_fail ((flags & INHERITS_OR_NULL_STDIN) != INHERITS_OR_NULL_STDIN, FALSE);
+  g_return_val_if_fail ((flags & INHERITS_OR_NULL_STDOUT) != INHERITS_OR_NULL_STDOUT, FALSE);
+  g_return_val_if_fail ((flags & INHERITS_OR_NULL_STDERR) != INHERITS_OR_NULL_STDERR, FALSE);
+  /* can’t use pipes and stdin/stdout/stderr FDs */
+  g_return_val_if_fail (stdin_pipe_out == NULL || stdin_fd < 0, FALSE);
+  g_return_val_if_fail (stdout_pipe_out == NULL || stdout_fd < 0, FALSE);
+  g_return_val_if_fail (stderr_pipe_out == NULL || stderr_fd < 0, FALSE);
+
+  if ((flags & INHERITS_OR_NULL_STDIN) != 0)
+    stdin_pipe_out = NULL;
+  if ((flags & INHERITS_OR_NULL_STDOUT) != 0)
+    stdout_pipe_out = NULL;
+  if ((flags & INHERITS_OR_NULL_STDERR) != 0)
+    stderr_pipe_out = NULL;
+
+  return fork_exec (!(flags & G_SPAWN_DO_NOT_REAP_CHILD),
+                    working_directory,
+                    (const gchar * const *) argv,
+                    (const gchar * const *) envp,
+                    !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
+                    (flags & G_SPAWN_SEARCH_PATH) != 0,
+                    (flags & G_SPAWN_SEARCH_PATH_FROM_ENVP) != 0,
+                    (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0,
+                    (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0,
+                    (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0,
+                    (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
+                    (flags & G_SPAWN_CLOEXEC_PIPES) != 0,
+                    child_setup,
+                    user_data,
+                    child_pid_out,
+                    stdin_pipe_out,
+                    stdout_pipe_out,
+                    stderr_pipe_out,
+                    stdin_fd,
+                    stdout_fd,
+                    stderr_fd,
+                    source_fds,
+                    target_fds,
+                    n_fds,
+                    error);
+}
+
+/**
+ * g_spawn_async_with_fds:
+ * @working_directory: (type filename) (nullable): child's current working directory, or %NULL to inherit parent's, in the GLib file name encoding
+ * @argv: (array zero-terminated=1): child's argument vector, in the GLib file name encoding;
+ *   it must be non-empty and %NULL-terminated
+ * @envp: (array zero-terminated=1) (nullable): child's environment, or %NULL to inherit parent's, in the GLib file name encoding
+ * @flags: flags from #GSpawnFlags
+ * @child_setup: (scope async) (closure user_data) (nullable): function to run
+ *   in the child just before `exec()`
+ * @user_data: user data for @child_setup
+ * @child_pid: (out) (optional): return location for child process ID, or %NULL
+ * @stdin_fd: file descriptor to use for child's stdin, or `-1`
+ * @stdout_fd: file descriptor to use for child's stdout, or `-1`
+ * @stderr_fd: file descriptor to use for child's stderr, or `-1`
+ * @error: return location for error
+ *
+ * Executes a child program asynchronously.
+ *
+ * Identical to g_spawn_async_with_pipes_and_fds() but with `n_fds` set to zero,
+ * so no FD assignments are used.
+ *
+ * Returns: %TRUE on success, %FALSE if an error was set
+ *
+ * Since: 2.58
+ */
+gboolean
+g_spawn_async_with_fds (const gchar          *working_directory,
+                        gchar               **argv,
+                        gchar               **envp,
+                        GSpawnFlags           flags,
+                        GSpawnChildSetupFunc  child_setup,
+                        gpointer              user_data,
+                        GPid                 *child_pid,
+                        gint                  stdin_fd,
+                        gint                  stdout_fd,
+                        gint                  stderr_fd,
+                        GError              **error)
+{
+  g_return_val_if_fail (argv != NULL, FALSE);
+  g_return_val_if_fail (argv[0] != NULL, FALSE);
+  g_return_val_if_fail (stdout_fd < 0 ||
                         !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE);
-  g_return_val_if_fail (standard_error == NULL ||
+  g_return_val_if_fail (stderr_fd < 0 ||
                         !(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE);
   /* can't inherit stdin if we have an input pipe. */
-  g_return_val_if_fail (standard_input == NULL ||
+  g_return_val_if_fail (stdin_fd < 0 ||
                         !(flags & G_SPAWN_CHILD_INHERITS_STDIN), FALSE);
-  
-  return fork_exec_with_pipes (!(flags & G_SPAWN_DO_NOT_REAP_CHILD),
-                               working_directory,
-                               argv,
-                               envp,
-                               !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
-                               (flags & G_SPAWN_SEARCH_PATH) != 0,
-                               (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0,
-                               (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0,
-                               (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0,
-                               (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
-                               child_setup,
-                               user_data,
-                               child_pid,
-                               standard_input,
-                               standard_output,
-                               standard_error,
-                               error);
+
+  return fork_exec (!(flags & G_SPAWN_DO_NOT_REAP_CHILD),
+                    working_directory,
+                    (const gchar * const *) argv,
+                    (const gchar * const *) envp,
+                    !(flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
+                    (flags & G_SPAWN_SEARCH_PATH) != 0,
+                    (flags & G_SPAWN_SEARCH_PATH_FROM_ENVP) != 0,
+                    (flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0,
+                    (flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0,
+                    (flags & G_SPAWN_CHILD_INHERITS_STDIN) != 0,
+                    (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
+                    (flags & G_SPAWN_CLOEXEC_PIPES) != 0,
+                    child_setup,
+                    user_data,
+                    child_pid,
+                    NULL, NULL, NULL,
+                    stdin_fd,
+                    stdout_fd,
+                    stderr_fd,
+                    NULL, NULL, 0,
+                    error);
 }
 
 /**
  * g_spawn_command_line_sync:
- * @command_line: a command line 
- * @standard_output: (out) (array zero-terminated=1) (element-type guint8) (allow-none): return location for child output
- * @standard_error: (out) (array zero-terminated=1) (element-type guint8) (allow-none): return location for child errors
- * @exit_status: (out) (allow-none): return location for child exit status, as returned by waitpid()
+ * @command_line: (type filename): a command line
+ * @standard_output: (out) (array zero-terminated=1) (element-type guint8) (optional): return location for child output
+ * @standard_error: (out) (array zero-terminated=1) (element-type guint8) (optional): return location for child errors
+ * @wait_status: (out) (optional): return location for child wait status, as returned by waitpid()
  * @error: return location for errors
  *
  * A simple version of g_spawn_sync() with little-used parameters
- * removed, taking a command line instead of an argument vector.  See
- * g_spawn_sync() for full details. @command_line will be parsed by
- * g_shell_parse_argv(). Unlike g_spawn_sync(), the %G_SPAWN_SEARCH_PATH flag
- * is enabled. Note that %G_SPAWN_SEARCH_PATH can have security
- * implications, so consider using g_spawn_sync() directly if
- * appropriate. Possible errors are those from g_spawn_sync() and those
+ * removed, taking a command line instead of an argument vector.
+ *
+ * See g_spawn_sync() for full details.
+ *
+ * The @command_line argument will be parsed by g_shell_parse_argv().
+ *
+ * Unlike g_spawn_sync(), the %G_SPAWN_SEARCH_PATH flag is enabled.
+ * Note that %G_SPAWN_SEARCH_PATH can have security implications, so
+ * consider using g_spawn_sync() directly if appropriate.
+ *
+ * Possible errors are those from g_spawn_sync() and those
  * from g_shell_parse_argv().
  *
- * If @exit_status is non-%NULL, the exit status of the child is stored there as
- * it would be returned by waitpid(); standard UNIX macros such as WIFEXITED()
- * and WEXITSTATUS() must be used to evaluate the exit status.
+ * If @wait_status is non-%NULL, the platform-specific status of
+ * the child is stored there; see the documentation of
+ * g_spawn_check_wait_status() for how to use and interpret this.
+ * On Unix platforms, note that it is usually not equal
+ * to the integer passed to `exit()` or returned from `main()`.
  * 
  * On Windows, please note the implications of g_shell_parse_argv()
  * parsing @command_line. Parsing is done according to Unix shell rules, not 
@@ -700,13 +1035,13 @@ g_spawn_async_with_pipes (const gchar          *working_directory,
  * separator. You need to enclose such paths with single quotes, like
  * "'c:\\program files\\app\\app.exe' 'e:\\folder\\argument.txt'".
  *
- * Return value: %TRUE on success, %FALSE if an error was set
+ * Returns: %TRUE on success, %FALSE if an error was set
  **/
 gboolean
 g_spawn_command_line_sync (const gchar  *command_line,
                            gchar       **standard_output,
                            gchar       **standard_error,
-                           gint         *exit_status,
+                           gint         *wait_status,
                            GError      **error)
 {
   gboolean retval;
@@ -714,6 +1049,7 @@ g_spawn_command_line_sync (const gchar  *command_line,
 
   g_return_val_if_fail (command_line != NULL, FALSE);
   
+  /* This will return a runtime error if @command_line is the empty string. */
   if (!g_shell_parse_argv (command_line,
                            NULL, &argv,
                            error))
@@ -727,7 +1063,7 @@ g_spawn_command_line_sync (const gchar  *command_line,
                          NULL,
                          standard_output,
                          standard_error,
-                         exit_status,
+                         wait_status,
                          error);
   g_strfreev (argv);
 
@@ -736,12 +1072,13 @@ g_spawn_command_line_sync (const gchar  *command_line,
 
 /**
  * g_spawn_command_line_async:
- * @command_line: a command line
+ * @command_line: (type filename): a command line
  * @error: return location for errors
  * 
  * A simple version of g_spawn_async() that parses a command line with
- * g_shell_parse_argv() and passes it to g_spawn_async(). Runs a
- * command line in the background. Unlike g_spawn_async(), the
+ * g_shell_parse_argv() and passes it to g_spawn_async().
+ *
+ * Runs a command line in the background. Unlike g_spawn_async(), the
  * %G_SPAWN_SEARCH_PATH flag is enabled, other flags are not. Note
  * that %G_SPAWN_SEARCH_PATH can have security implications, so
  * consider using g_spawn_async() directly if appropriate. Possible
@@ -749,7 +1086,7 @@ g_spawn_command_line_sync (const gchar  *command_line,
  * 
  * The same concerns on Windows apply as for g_spawn_command_line_sync().
  *
- * Return value: %TRUE on success, %FALSE if error is set.
+ * Returns: %TRUE on success, %FALSE if error is set
  **/
 gboolean
 g_spawn_command_line_async (const gchar *command_line,
@@ -760,6 +1097,7 @@ g_spawn_command_line_async (const gchar *command_line,
 
   g_return_val_if_fail (command_line != NULL, FALSE);
 
+  /* This will return a runtime error if @command_line is the empty string. */
   if (!g_shell_parse_argv (command_line,
                            NULL, &argv,
                            error))
@@ -778,113 +1116,128 @@ g_spawn_command_line_async (const gchar *command_line,
   return retval;
 }
 
-static gint
-exec_err_to_g_error (gint en)
+/**
+ * g_spawn_check_wait_status:
+ * @wait_status: A platform-specific wait status as returned from g_spawn_sync()
+ * @error: a #GError
+ *
+ * Set @error if @wait_status indicates the child exited abnormally
+ * (e.g. with a nonzero exit code, or via a fatal signal).
+ *
+ * The g_spawn_sync() and g_child_watch_add() family of APIs return the
+ * status of subprocesses encoded in a platform-specific way.
+ * On Unix, this is guaranteed to be in the same format waitpid() returns,
+ * and on Windows it is guaranteed to be the result of GetExitCodeProcess().
+ *
+ * Prior to the introduction of this function in GLib 2.34, interpreting
+ * @wait_status required use of platform-specific APIs, which is problematic
+ * for software using GLib as a cross-platform layer.
+ *
+ * Additionally, many programs simply want to determine whether or not
+ * the child exited successfully, and either propagate a #GError or
+ * print a message to standard error. In that common case, this function
+ * can be used. Note that the error message in @error will contain
+ * human-readable information about the wait status.
+ *
+ * The @domain and @code of @error have special semantics in the case
+ * where the process has an "exit code", as opposed to being killed by
+ * a signal. On Unix, this happens if WIFEXITED() would be true of
+ * @wait_status. On Windows, it is always the case.
+ *
+ * The special semantics are that the actual exit code will be the
+ * code set in @error, and the domain will be %G_SPAWN_EXIT_ERROR.
+ * This allows you to differentiate between different exit codes.
+ *
+ * If the process was terminated by some means other than an exit
+ * status (for example if it was killed by a signal), the domain will be
+ * %G_SPAWN_ERROR and the code will be %G_SPAWN_ERROR_FAILED.
+ *
+ * This function just offers convenience; you can of course also check
+ * the available platform via a macro such as %G_OS_UNIX, and use
+ * WIFEXITED() and WEXITSTATUS() on @wait_status directly. Do not attempt
+ * to scan or parse the error message string; it may be translated and/or
+ * change in future versions of GLib.
+ *
+ * Prior to version 2.70, g_spawn_check_exit_status() provides the same
+ * functionality, although under a misleading name.
+ *
+ * Returns: %TRUE if child exited successfully, %FALSE otherwise (and
+ *   @error will be set)
+ *
+ * Since: 2.70
+ */
+gboolean
+g_spawn_check_wait_status (gint      wait_status,
+                          GError  **error)
 {
-  switch (en)
-    {
-#ifdef EACCES
-    case EACCES:
-      return G_SPAWN_ERROR_ACCES;
-      break;
-#endif
-
-#ifdef EPERM
-    case EPERM:
-      return G_SPAWN_ERROR_PERM;
-      break;
-#endif
-
-#ifdef E2BIG
-    case E2BIG:
-      return G_SPAWN_ERROR_2BIG;
-      break;
-#endif
+  gboolean ret = FALSE;
 
-#ifdef ENOEXEC
-    case ENOEXEC:
-      return G_SPAWN_ERROR_NOEXEC;
-      break;
-#endif
-
-#ifdef ENAMETOOLONG
-    case ENAMETOOLONG:
-      return G_SPAWN_ERROR_NAMETOOLONG;
-      break;
-#endif
-
-#ifdef ENOENT
-    case ENOENT:
-      return G_SPAWN_ERROR_NOENT;
-      break;
-#endif
-
-#ifdef ENOMEM
-    case ENOMEM:
-      return G_SPAWN_ERROR_NOMEM;
-      break;
-#endif
-
-#ifdef ENOTDIR
-    case ENOTDIR:
-      return G_SPAWN_ERROR_NOTDIR;
-      break;
-#endif
-
-#ifdef ELOOP
-    case ELOOP:
-      return G_SPAWN_ERROR_LOOP;
-      break;
-#endif
-      
-#ifdef ETXTBUSY
-    case ETXTBUSY:
-      return G_SPAWN_ERROR_TXTBUSY;
-      break;
-#endif
-
-#ifdef EIO
-    case EIO:
-      return G_SPAWN_ERROR_IO;
-      break;
-#endif
-
-#ifdef ENFILE
-    case ENFILE:
-      return G_SPAWN_ERROR_NFILE;
-      break;
-#endif
-
-#ifdef EMFILE
-    case EMFILE:
-      return G_SPAWN_ERROR_MFILE;
-      break;
-#endif
-
-#ifdef EINVAL
-    case EINVAL:
-      return G_SPAWN_ERROR_INVAL;
-      break;
-#endif
+  if (WIFEXITED (wait_status))
+    {
+      if (WEXITSTATUS (wait_status) != 0)
+       {
+         g_set_error (error, G_SPAWN_EXIT_ERROR, WEXITSTATUS (wait_status),
+                      _("Child process exited with code %ld"),
+                      (long) WEXITSTATUS (wait_status));
+         goto out;
+       }
+    }
+  else if (WIFSIGNALED (wait_status))
+    {
+      g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
+                  _("Child process killed by signal %ld"),
+                  (long) WTERMSIG (wait_status));
+      goto out;
+    }
+  else if (WIFSTOPPED (wait_status))
+    {
+      g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
+                  _("Child process stopped by signal %ld"),
+                  (long) WSTOPSIG (wait_status));
+      goto out;
+    }
+  else
+    {
+      g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
+                  _("Child process exited abnormally"));
+      goto out;
+    }
 
-#ifdef EISDIR
-    case EISDIR:
-      return G_SPAWN_ERROR_ISDIR;
-      break;
-#endif
+  ret = TRUE;
+ out:
+  return ret;
+}
 
-#ifdef ELIBBAD
-    case ELIBBAD:
-      return G_SPAWN_ERROR_LIBBAD;
-      break;
-#endif
-      
-    default:
-      return G_SPAWN_ERROR_FAILED;
-      break;
-    }
+/**
+ * g_spawn_check_exit_status:
+ * @wait_status: A status as returned from g_spawn_sync()
+ * @error: a #GError
+ *
+ * An old name for g_spawn_check_wait_status(), deprecated because its
+ * name is misleading.
+ *
+ * Despite the name of the function, @wait_status must be the wait status
+ * as returned by g_spawn_sync(), g_subprocess_get_status(), `waitpid()`,
+ * etc. On Unix platforms, it is incorrect for it to be the exit status
+ * as passed to `exit()` or returned by g_subprocess_get_exit_status() or
+ * `WEXITSTATUS()`.
+ *
+ * Returns: %TRUE if child exited successfully, %FALSE otherwise (and
+ *     @error will be set)
+ *
+ * Since: 2.34
+ *
+ * Deprecated: 2.70: Use g_spawn_check_wait_status() instead, and check whether your code is conflating wait and exit statuses.
+ */
+gboolean
+g_spawn_check_exit_status (gint      wait_status,
+                           GError  **error)
+{
+  return g_spawn_check_wait_status (wait_status, error);
 }
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
 static gssize
 write_all (gint fd, gconstpointer vbuf, gsize to_write)
 {
@@ -908,7 +1261,9 @@ write_all (gint fd, gconstpointer vbuf, gsize to_write)
   return TRUE;
 }
 
-G_GNUC_NORETURN
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+G_NORETURN
 static void
 write_err_and_exit (gint fd, gint msg)
 {
@@ -920,6 +1275,8 @@ write_err_and_exit (gint fd, gint msg)
   _exit (1);
 }
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
 static int
 set_cloexec (void *data, gint fd)
 {
@@ -929,64 +1286,305 @@ set_cloexec (void *data, gint fd)
   return 0;
 }
 
-#ifndef HAVE_FDWALK
-static int
-fdwalk (int (*cb)(void *data, int fd), void *data)
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static void
+unset_cloexec (int fd)
 {
-  gint open_max;
-  gint fd;
-  gint res = 0;
-  
-#ifdef HAVE_SYS_RESOURCE_H
-  struct rlimit rl;
-#endif
+  int flags;
+  int result;
 
-#ifdef __linux__  
-  DIR *d;
+  flags = fcntl (fd, F_GETFD, 0);
 
-  if ((d = opendir("/proc/self/fd"))) {
-      struct dirent *de;
+  if (flags != -1)
+    {
+      int errsv;
+      flags &= (~FD_CLOEXEC);
+      do
+        {
+          result = fcntl (fd, F_SETFD, flags);
+          errsv = errno;
+        }
+      while (result == -1 && errsv == EINTR);
+    }
+}
 
-      while ((de = readdir(d))) {
-          glong l;
-          gchar *e = NULL;
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static int
+dupfd_cloexec (int old_fd, int new_fd_min)
+{
+  int fd, errsv;
+#ifdef F_DUPFD_CLOEXEC
+  do
+    {
+      fd = fcntl (old_fd, F_DUPFD_CLOEXEC, new_fd_min);
+      errsv = errno;
+    }
+  while (fd == -1 && errsv == EINTR);
+#else
+  /* OS X Snow Lion and earlier don't have F_DUPFD_CLOEXEC:
+   * https://bugzilla.gnome.org/show_bug.cgi?id=710962
+   */
+  int result, flags;
+  do
+    {
+      fd = fcntl (old_fd, F_DUPFD, new_fd_min);
+      errsv = errno;
+    }
+  while (fd == -1 && errsv == EINTR);
+  flags = fcntl (fd, F_GETFD, 0);
+  if (flags != -1)
+    {
+      flags |= FD_CLOEXEC;
+      do
+        {
+          result = fcntl (fd, F_SETFD, flags);
+          errsv = errno;
+        }
+      while (result == -1 && errsv == EINTR);
+    }
+#endif
+  return fd;
+}
 
-          if (de->d_name[0] == '.')
-              continue;
-            
-          errno = 0;
-          l = strtol(de->d_name, &e, 10);
-          if (errno != 0 || !e || *e)
-              continue;
+/* fdwalk()-compatible callback to close a fd for non-compliant
+ * implementations of fdwalk() that potentially pass already
+ * closed fds.
+ *
+ * It is not an error to pass an invalid fd to this function.
+ *
+ * This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)).
+ */
+G_GNUC_UNUSED static int
+close_func_with_invalid_fds (void *data, int fd)
+{
+  /* We use close and not g_close here because on some platforms, we
+   * don't know how to close only valid, open file descriptors, so we
+   * have to pass bad fds to close too. g_close warns if given a bad
+   * fd.
+   *
+   * This function returns no error, because there is nothing that the caller
+   * could do with that information. That is even the case for EINTR. See
+   * g_close() about the specialty of EINTR and why that is correct.
+   * If g_close() ever gets extended to handle EINTR specially, then this place
+   * should get updated to do the same handling.
+   */
+  if (fd >= GPOINTER_TO_INT (data))
+    close (fd);
 
-          fd = (gint) l;
+  return 0;
+}
 
-          if ((glong) fd != l)
-              continue;
+#ifdef __linux__
+struct linux_dirent64
+{
+  guint64        d_ino;    /* 64-bit inode number */
+  guint64        d_off;    /* 64-bit offset to next structure */
+  unsigned short d_reclen; /* Size of this dirent */
+  unsigned char  d_type;   /* File type */
+  char           d_name[]; /* Filename (null-terminated) */
+};
 
-          if (fd == dirfd(d))
-              continue;
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static gint
+filename_to_fd (const char *p)
+{
+  char c;
+  int fd = 0;
+  const int cutoff = G_MAXINT / 10;
+  const int cutlim = G_MAXINT % 10;
 
-          if ((res = cb (data, fd)) != 0)
-              break;
+  if (*p == '\0')
+    return -1;
+
+  while ((c = *p++) != '\0')
+    {
+      if (c < '0' || c > '9')
+        return -1;
+      c -= '0';
+
+      /* Check for overflow. */
+      if (fd > cutoff || (fd == cutoff && c > cutlim))
+        return -1;
+
+      fd = fd * 10 + c;
+    }
+
+  return fd;
+}
+#endif
+
+static int safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data);
+
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static int
+safe_fdwalk (int (*cb)(void *data, int fd), void *data)
+{
+#if 0
+  /* Use fdwalk function provided by the system if it is known to be
+   * async-signal safe.
+   *
+   * Currently there are no operating systems known to provide a safe
+   * implementation, so this section is not used for now.
+   */
+  return fdwalk (cb, data);
+#else
+  /* Fallback implementation of fdwalk. It should be async-signal safe, but it
+   * may fail on non-Linux operating systems. See safe_fdwalk_with_invalid_fds
+   * for a slower alternative.
+   */
+
+#ifdef __linux__
+  gint fd;
+  gint res = 0;
+
+  /* Avoid use of opendir/closedir since these are not async-signal-safe. */
+  int dir_fd = open ("/proc/self/fd", O_RDONLY | O_DIRECTORY);
+  if (dir_fd >= 0)
+    {
+      /* buf needs to be aligned correctly to receive linux_dirent64.
+       * C11 has _Alignof for this purpose, but for now a
+       * union serves the same purpose. */
+      union
+      {
+        char buf[4096];
+        struct linux_dirent64 alignment;
+      } u;
+      int pos, nread;
+      struct linux_dirent64 *de;
+
+      while ((nread = syscall (SYS_getdents64, dir_fd, u.buf, sizeof (u.buf))) > 0)
+        {
+          for (pos = 0; pos < nread; pos += de->d_reclen)
+            {
+              de = (struct linux_dirent64 *) (u.buf + pos);
+
+              fd = filename_to_fd (de->d_name);
+              if (fd < 0 || fd == dir_fd)
+                  continue;
+
+              if ((res = cb (data, fd)) != 0)
+                  break;
+            }
         }
-      
-      closedir(d);
+
+      g_close (dir_fd, NULL);
       return res;
-  }
+    }
 
-  /* If /proc is not mounted or not accessible we fall back to the old
-   * rlimit trick */
+  /* If /proc is not mounted or not accessible we fail here and rely on
+   * safe_fdwalk_with_invalid_fds to fall back to the old
+   * rlimit trick. */
 
 #endif
-  
-#ifdef HAVE_SYS_RESOURCE_H
-      
-  if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY)
-      open_max = rl.rlim_max;
-  else
+
+#if defined(__sun__) && defined(F_PREVFD) && defined(F_NEXTFD)
+/*
+ * Solaris 11.4 has a signal-safe way which allows
+ * us to find all file descriptors in a process.
+ *
+ * fcntl(fd, F_NEXTFD, maxfd)
+ * - returns the first allocated file descriptor <= maxfd  > fd.
+ *
+ * fcntl(fd, F_PREVFD)
+ * - return highest allocated file descriptor < fd.
+ */
+  gint fd;
+  gint res = 0;
+
+  open_max = fcntl (INT_MAX, F_PREVFD); /* find the maximum fd */
+  if (open_max < 0) /* No open files */
+    return 0;
+
+  for (fd = -1; (fd = fcntl (fd, F_NEXTFD, open_max)) != -1; )
+    if ((res = cb (data, fd)) != 0 || fd == open_max)
+      break;
+
+  return res;
+#endif
+
+  return safe_fdwalk_with_invalid_fds (cb, data);
+#endif
+}
+
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static int
+safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data)
+{
+  /* Fallback implementation of fdwalk. It should be async-signal safe, but it
+   * may be slow, especially on systems allowing very high number of open file
+   * descriptors.
+   */
+  gint open_max = -1;
+  gint fd;
+  gint res = 0;
+
+#if 0 && defined(HAVE_SYS_RESOURCE_H)
+  struct rlimit rl;
+
+  /* Use getrlimit() function provided by the system if it is known to be
+   * async-signal safe.
+   *
+   * Currently there are no operating systems known to provide a safe
+   * implementation, so this section is not used for now.
+   */
+  if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY)
+    open_max = rl.rlim_max;
+#endif
+#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__)
+  /* Use sysconf() function provided by the system if it is known to be
+   * async-signal safe.
+   *
+   * FreeBSD: sysconf() is included in the list of async-signal safe functions
+   * found in https://man.freebsd.org/sigaction(2).
+   *
+   * OpenBSD: sysconf() is included in the list of async-signal safe functions
+   * found in https://man.openbsd.org/sigaction.2.
+   * 
+   * Apple: sysconf() is included in the list of async-signal safe functions
+   * found in https://opensource.apple.com/source/xnu/xnu-517.12.7/bsd/man/man2/sigaction.2
+   */
+  if (open_max < 0)
+    open_max = sysconf (_SC_OPEN_MAX);
+#endif
+  /* Hardcoded fallback: the default process hard limit in Linux as of 2020 */
+  if (open_max < 0)
+    open_max = 4096;
+
+#if defined(__APPLE__)
+  /* proc_pidinfo isn't documented as async-signal-safe but looking at the implementation
+   * in the darwin tree here:
+   *
+   * https://opensource.apple.com/source/Libc/Libc-498/darwin/libproc.c.auto.html
+   *
+   * It's just a thin wrapper around a syscall, so it's probably okay.
+   */
+  {
+    char buffer[4096 * PROC_PIDLISTFD_SIZE];
+    ssize_t buffer_size;
+
+    buffer_size = proc_pidinfo (getpid (), PROC_PIDLISTFDS, 0, buffer, sizeof (buffer));
+
+    if (buffer_size > 0 &&
+        sizeof (buffer) >= (size_t) buffer_size &&
+        (buffer_size % PROC_PIDLISTFD_SIZE) == 0)
+      {
+        const struct proc_fdinfo *fd_info = (const struct proc_fdinfo *) buffer;
+        size_t number_of_fds = (size_t) buffer_size / PROC_PIDLISTFD_SIZE;
+
+        for (size_t i = 0; i < number_of_fds; i++)
+          if ((res = cb (data, fd_info[i].proc_fd)) != 0)
+            break;
+
+        return res;
+      }
+  }
 #endif
-      open_max = sysconf (_SC_OPEN_MAX);
 
   for (fd = 0; fd < open_max; fd++)
       if ((res = cb (data, fd)) != 0)
@@ -994,30 +1592,141 @@ fdwalk (int (*cb)(void *data, int fd), void *data)
 
   return res;
 }
+
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static int
+safe_fdwalk_set_cloexec (int lowfd)
+{
+  int ret;
+
+#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC)
+  /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at
+   * around the same time. It was designed for use in async-signal-safe
+   * situations: https://bugs.python.org/issue38061
+   *
+   * The `CLOSE_RANGE_CLOEXEC` flag was added in Linux 5.11, and is not yet
+   * present in FreeBSD.
+   *
+   * Handle ENOSYS in case it’s supported in libc but not the kernel; if so,
+   * fall back to safe_fdwalk(). Handle EINVAL in case `CLOSE_RANGE_CLOEXEC`
+   * is not supported. */
+  ret = close_range (lowfd, G_MAXUINT, CLOSE_RANGE_CLOEXEC);
+  if (ret == 0 || !(errno == ENOSYS || errno == EINVAL))
+    return ret;
+#endif  /* HAVE_CLOSE_RANGE */
+
+  ret = safe_fdwalk (set_cloexec, GINT_TO_POINTER (lowfd));
+
+  return ret;
+}
+
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)).
+ *
+ * On failure, `-1` will be returned and errno will be set. */
+static int
+safe_closefrom (int lowfd)
+{
+  int ret;
+
+#if defined(HAVE_CLOSE_RANGE)
+  /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at
+   * around the same time. It was designed for use in async-signal-safe
+   * situations: https://bugs.python.org/issue38061
+   *
+   * Handle ENOSYS in case it’s supported in libc but not the kernel; if so,
+   * fall back to safe_fdwalk(). */
+  ret = close_range (lowfd, G_MAXUINT, 0);
+  if (ret == 0 || errno != ENOSYS)
+    return ret;
+#endif  /* HAVE_CLOSE_RANGE */
+
+#if defined(__FreeBSD__) || defined(__OpenBSD__) || \
+  (defined(__sun__) && defined(F_CLOSEFROM))
+  /* Use closefrom function provided by the system if it is known to be
+   * async-signal safe.
+   *
+   * FreeBSD: closefrom is included in the list of async-signal safe functions
+   * found in https://man.freebsd.org/sigaction(2).
+   *
+   * OpenBSD: closefrom is not included in the list, but a direct system call
+   * should be safe to use.
+   *
+   * In Solaris as of 11.3 SRU 31, closefrom() is also a direct system call.
+   * On such systems, F_CLOSEFROM is defined.
+   */
+  (void) closefrom (lowfd);
+  return 0;
+#elif defined(__DragonFly__)
+  /* It is unclear whether closefrom function included in DragonFlyBSD libc_r
+   * is safe to use because it calls a lot of library functions. It is also
+   * unclear whether libc_r itself is still being used. Therefore, we do a
+   * direct system call here ourselves to avoid possible issues.
+   */
+  (void) syscall (SYS_closefrom, lowfd);
+  return 0;
+#elif defined(F_CLOSEM)
+  /* NetBSD and AIX have a special fcntl command which does the same thing as
+   * closefrom. NetBSD also includes closefrom function, which seems to be a
+   * simple wrapper of the fcntl command.
+   */
+  return fcntl (lowfd, F_CLOSEM);
+#else
+  ret = safe_fdwalk (close_func_with_invalid_fds, GINT_TO_POINTER (lowfd));
+
+  return ret;
 #endif
+}
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
 static gint
-sane_dup2 (gint fd1, gint fd2)
+safe_dup2 (gint fd1, gint fd2)
 {
   gint ret;
 
- retry:
-  ret = dup2 (fd1, fd2);
-  if (ret < 0 && errno == EINTR)
-    goto retry;
+  do
+    ret = dup2 (fd1, fd2);
+  while (ret < 0 && (errno == EINTR || errno == EBUSY));
 
   return ret;
 }
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
+static gboolean
+relocate_fd_out_of_standard_range (gint *fd)
+{
+  gint ret = -1;
+  const int min_fileno = STDERR_FILENO + 1;
+
+  do
+    ret = fcntl (*fd, F_DUPFD, min_fileno);
+  while (ret < 0 && errno == EINTR);
+
+  /* Note we don't need to close the old fd, because the caller is expected
+   * to close fds in the standard range itself.
+   */
+  if (ret >= min_fileno)
+    {
+      *fd = ret;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
 static gint
-sane_open (const char *path, gint mode)
+safe_open (const char *path, gint mode)
 {
   gint ret;
 
- retry:
-  ret = open (path, mode);
-  if (ret < 0 && errno == EINTR)
-    goto retry;
+  do
+    ret = open (path, mode);
+  while (ret < 0 && errno == EINTR);
 
   return ret;
 }
@@ -1026,20 +1735,33 @@ enum
 {
   CHILD_CHDIR_FAILED,
   CHILD_EXEC_FAILED,
-  CHILD_DUP2_FAILED,
-  CHILD_FORK_FAILED
+  CHILD_OPEN_FAILED,
+  CHILD_DUPFD_FAILED,
+  CHILD_FORK_FAILED,
+  CHILD_CLOSE_FAILED,
 };
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)) until it calls exec().
+ *
+ * All callers must guarantee that @argv and @argv[0] are non-NULL. */
 static void
 do_exec (gint                  child_err_report_fd,
          gint                  stdin_fd,
          gint                  stdout_fd,
          gint                  stderr_fd,
+         gint                 *source_fds,
+         const gint           *target_fds,
+         gsize                 n_fds,
          const gchar          *working_directory,
-         gchar               **argv,
-         gchar               **envp,
+         const gchar * const  *argv,
+         gchar               **argv_buffer,
+         gsize                 argv_buffer_len,
+         const gchar * const  *envp,
          gboolean              close_descriptors,
-         gboolean              search_path,
+         const gchar          *search_path,
+         gchar                *search_path_buffer,
+         gsize                 search_path_buffer_len,
          gboolean              stdout_to_null,
          gboolean              stderr_to_null,
          gboolean              child_inherits_stdin,
@@ -1047,82 +1769,224 @@ do_exec (gint                  child_err_report_fd,
          GSpawnChildSetupFunc  child_setup,
          gpointer              user_data)
 {
+  gsize i;
+  gint max_target_fd = 0;
+
   if (working_directory && chdir (working_directory) < 0)
     write_err_and_exit (child_err_report_fd,
                         CHILD_CHDIR_FAILED);
 
-  /* Close all file descriptors but stdin stdout and stderr as
-   * soon as we exec. Note that this includes
-   * child_err_report_fd, which keeps the parent from blocking
-   * forever on the other end of that pipe.
+  /* It's possible the caller assigned stdin to an fd with a
+   * file number that is supposed to be reserved for
+   * stdout or stderr.
+   *
+   * If so, move it up out of the standard range, so it doesn't
+   * cause a conflict.
    */
-  if (close_descriptors)
+  if (IS_STD_FILENO (stdin_fd) && stdin_fd != STDIN_FILENO)
     {
-      fdwalk (set_cloexec, GINT_TO_POINTER(3));
-    }
-  else
-    {
-      /* We need to do child_err_report_fd anyway */
-      set_cloexec (GINT_TO_POINTER(0), child_err_report_fd);
+      int old_fd = stdin_fd;
+
+      if (!relocate_fd_out_of_standard_range (&stdin_fd))
+        write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+
+      if (stdout_fd == old_fd)
+        stdout_fd = stdin_fd;
+
+      if (stderr_fd == old_fd)
+        stderr_fd = stdin_fd;
     }
-  
-  /* Redirect pipes as required */
-  
-  if (stdin_fd >= 0)
+
+  /* Redirect pipes as required
+   *
+   * There are two cases where we don't need to do the redirection
+   * 1. Where the associated file descriptor is cleared/invalid
+   * 2. When the associated file descriptor is already given the
+   * correct file number.
+   */
+  if (IS_VALID_FILENO (stdin_fd) && stdin_fd != STDIN_FILENO)
     {
-      /* dup2 can't actually fail here I don't think */
-          
-      if (sane_dup2 (stdin_fd, 0) < 0)
+      if (safe_dup2 (stdin_fd, 0) < 0)
         write_err_and_exit (child_err_report_fd,
-                            CHILD_DUP2_FAILED);
+                            CHILD_DUPFD_FAILED);
 
-      /* ignore this if it doesn't work */
-      close_and_invalidate (&stdin_fd);
+      set_cloexec (GINT_TO_POINTER(0), stdin_fd);
     }
   else if (!child_inherits_stdin)
     {
       /* Keep process from blocking on a read of stdin */
-      gint read_null = open ("/dev/null", O_RDONLY);
-      sane_dup2 (read_null, 0);
+      gint read_null = safe_open ("/dev/null", O_RDONLY);
+      if (read_null < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_OPEN_FAILED);
+      if (safe_dup2 (read_null, 0) < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_DUPFD_FAILED);
       close_and_invalidate (&read_null);
     }
 
-  if (stdout_fd >= 0)
+  /* Like with stdin above, it's possible the caller assigned
+   * stdout to an fd with a file number that's intruding on the
+   * standard range.
+   *
+   * If so, move it out of the way, too.
+   */
+  if (IS_STD_FILENO (stdout_fd) && stdout_fd != STDOUT_FILENO)
     {
-      /* dup2 can't actually fail here I don't think */
-          
-      if (sane_dup2 (stdout_fd, 1) < 0)
+      int old_fd = stdout_fd;
+
+      if (!relocate_fd_out_of_standard_range (&stdout_fd))
+        write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+
+      if (stderr_fd == old_fd)
+        stderr_fd = stdout_fd;
+    }
+
+  if (IS_VALID_FILENO (stdout_fd) && stdout_fd != STDOUT_FILENO)
+    {
+      if (safe_dup2 (stdout_fd, 1) < 0)
         write_err_and_exit (child_err_report_fd,
-                            CHILD_DUP2_FAILED);
+                            CHILD_DUPFD_FAILED);
 
-      /* ignore this if it doesn't work */
-      close_and_invalidate (&stdout_fd);
+      set_cloexec (GINT_TO_POINTER(0), stdout_fd);
     }
   else if (stdout_to_null)
     {
-      gint write_null = sane_open ("/dev/null", O_WRONLY);
-      sane_dup2 (write_null, 1);
+      gint write_null = safe_open ("/dev/null", O_WRONLY);
+      if (write_null < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_OPEN_FAILED);
+      if (safe_dup2 (write_null, 1) < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_DUPFD_FAILED);
       close_and_invalidate (&write_null);
     }
 
-  if (stderr_fd >= 0)
+  if (IS_STD_FILENO (stderr_fd) && stderr_fd != STDERR_FILENO)
     {
-      /* dup2 can't actually fail here I don't think */
-          
-      if (sane_dup2 (stderr_fd, 2) < 0)
+      if (!relocate_fd_out_of_standard_range (&stderr_fd))
+        write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+    }
+
+  /* Like with stdin/stdout above, it's possible the caller assigned
+   * stderr to an fd with a file number that's intruding on the
+   * standard range.
+   *
+   * Make sure it's out of the way, also.
+   */
+  if (IS_VALID_FILENO (stderr_fd) && stderr_fd != STDERR_FILENO)
+    {
+      if (safe_dup2 (stderr_fd, 2) < 0)
         write_err_and_exit (child_err_report_fd,
-                            CHILD_DUP2_FAILED);
+                            CHILD_DUPFD_FAILED);
 
-      /* ignore this if it doesn't work */
-      close_and_invalidate (&stderr_fd);
+      set_cloexec (GINT_TO_POINTER(0), stderr_fd);
     }
   else if (stderr_to_null)
     {
-      gint write_null = sane_open ("/dev/null", O_WRONLY);
-      sane_dup2 (write_null, 2);
+      gint write_null = safe_open ("/dev/null", O_WRONLY);
+      if (write_null < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_OPEN_FAILED);
+      if (safe_dup2 (write_null, 2) < 0)
+        write_err_and_exit (child_err_report_fd,
+                            CHILD_DUPFD_FAILED);
       close_and_invalidate (&write_null);
     }
-  
+
+  /* Close all file descriptors but stdin, stdout and stderr, and any of source_fds,
+   * before we exec. Note that this includes
+   * child_err_report_fd, which keeps the parent from blocking
+   * forever on the other end of that pipe.
+   */
+  if (close_descriptors)
+    {
+      if (child_setup == NULL && n_fds == 0)
+        {
+          if (safe_dup2 (child_err_report_fd, 3) < 0)
+            write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+          set_cloexec (GINT_TO_POINTER (0), 3);
+          if (safe_closefrom (4) < 0)
+            write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED);
+          child_err_report_fd = 3;
+        }
+      else
+        {
+          if (safe_fdwalk_set_cloexec (3) < 0)
+            write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED);
+        }
+    }
+  else
+    {
+      /* We need to do child_err_report_fd anyway */
+      set_cloexec (GINT_TO_POINTER (0), child_err_report_fd);
+    }
+
+  /*
+   * Work through the @source_fds and @target_fds mapping.
+   *
+   * Based on code originally derived from
+   * gnome-terminal:src/terminal-screen.c:terminal_screen_child_setup(),
+   * used under the LGPLv2+ with permission from author. (The code has
+   * since migrated to vte:src/spawn.cc:SpawnContext::exec and is no longer
+   * terribly similar to what we have here.)
+   */
+
+  if (n_fds > 0)
+    {
+      for (i = 0; i < n_fds; i++)
+        max_target_fd = MAX (max_target_fd, target_fds[i]);
+
+      if (max_target_fd == G_MAXINT)
+        {
+          errno = EINVAL;
+          write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+        }
+
+      /* If we're doing remapping fd assignments, we need to handle
+       * the case where the user has specified e.g. 5 -> 4, 4 -> 6.
+       * We do this by duping all source fds, taking care to ensure the new
+       * fds are larger than any target fd to avoid introducing new conflicts.
+       */
+      for (i = 0; i < n_fds; i++)
+        {
+          if (source_fds[i] != target_fds[i])
+            {
+              source_fds[i] = dupfd_cloexec (source_fds[i], max_target_fd + 1);
+              if (source_fds[i] < 0)
+                write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+            }
+        }
+
+      for (i = 0; i < n_fds; i++)
+        {
+          /* For basic fd assignments (where source == target), we can just
+           * unset FD_CLOEXEC.
+           */
+          if (source_fds[i] == target_fds[i])
+            {
+              unset_cloexec (source_fds[i]);
+            }
+          else
+            {
+              /* If any of the @target_fds conflict with @child_err_report_fd,
+               * dup it so it doesn’t get conflated.
+               */
+              if (target_fds[i] == child_err_report_fd)
+                {
+                  child_err_report_fd = dupfd_cloexec (child_err_report_fd, max_target_fd + 1);
+                  if (child_err_report_fd < 0)
+                    write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+                }
+
+              if (safe_dup2 (source_fds[i], target_fds[i]) < 0)
+                write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
+
+              close_and_invalidate (&source_fds[i]);
+            }
+        }
+    }
+
   /* Call user function just before we exec */
   if (child_setup)
     {
@@ -1130,8 +1994,9 @@ do_exec (gint                  child_err_report_fd,
     }
 
   g_execute (argv[0],
-             file_and_argv_zero ? argv + 1 : argv,
-             envp, search_path);
+             (gchar **) (file_and_argv_zero ? argv + 1 : argv),
+             argv_buffer, argv_buffer_len,
+             (gchar **) envp, search_path, search_path_buffer, search_path_buffer_len);
 
   /* Exec failed */
   write_err_and_exit (child_err_report_fd,
@@ -1187,48 +2052,479 @@ read_ints (int      fd,
   return TRUE;
 }
 
+#ifdef POSIX_SPAWN_AVAILABLE
+static gboolean
+do_posix_spawn (const gchar * const *argv,
+                const gchar * const *envp,
+                gboolean    search_path,
+                gboolean    stdout_to_null,
+                gboolean    stderr_to_null,
+                gboolean    child_inherits_stdin,
+                gboolean    file_and_argv_zero,
+                GPid       *child_pid,
+                gint       *child_close_fds,
+                gint        stdin_fd,
+                gint        stdout_fd,
+                gint        stderr_fd,
+                const gint *source_fds,
+                const gint *target_fds,
+                gsize       n_fds)
+{
+  pid_t pid;
+  gint *duped_source_fds = NULL;
+  gint max_target_fd = 0;
+  const gchar * const *argv_pass;
+  posix_spawnattr_t attr;
+  posix_spawn_file_actions_t file_actions;
+  gint parent_close_fds[3];
+  gsize num_parent_close_fds = 0;
+  GSList *child_close = NULL;
+  GSList *elem;
+  sigset_t mask;
+  gsize i;
+  int r;
+
+  g_assert (argv != NULL && argv[0] != NULL);
+
+  if (*argv[0] == '\0')
+    {
+      /* We check the simple case first. */
+      return ENOENT;
+    }
+
+  r = posix_spawnattr_init (&attr);
+  if (r != 0)
+    return r;
+
+  if (child_close_fds)
+    {
+      int i = -1;
+      while (child_close_fds[++i] != -1)
+        child_close = g_slist_prepend (child_close,
+                                       GINT_TO_POINTER (child_close_fds[i]));
+    }
+
+  r = posix_spawnattr_setflags (&attr, POSIX_SPAWN_SETSIGDEF);
+  if (r != 0)
+    goto out_free_spawnattr;
+
+  /* Reset some signal handlers that we may use */
+  sigemptyset (&mask);
+  sigaddset (&mask, SIGCHLD);
+  sigaddset (&mask, SIGINT);
+  sigaddset (&mask, SIGTERM);
+  sigaddset (&mask, SIGHUP);
+
+  r = posix_spawnattr_setsigdefault (&attr, &mask);
+  if (r != 0)
+    goto out_free_spawnattr;
+
+  r = posix_spawn_file_actions_init (&file_actions);
+  if (r != 0)
+    goto out_free_spawnattr;
+
+  /* Redirect pipes as required */
+
+  if (stdin_fd >= 0)
+    {
+      r = posix_spawn_file_actions_adddup2 (&file_actions, stdin_fd, 0);
+      if (r != 0)
+        goto out_close_fds;
+
+      if (!g_slist_find (child_close, GINT_TO_POINTER (stdin_fd)))
+        child_close = g_slist_prepend (child_close, GINT_TO_POINTER (stdin_fd));
+    }
+  else if (!child_inherits_stdin)
+    {
+      /* Keep process from blocking on a read of stdin */
+      gint read_null = safe_open ("/dev/null", O_RDONLY | O_CLOEXEC);
+      g_assert (read_null != -1);
+      parent_close_fds[num_parent_close_fds++] = read_null;
+
+#ifndef HAVE_O_CLOEXEC
+      fcntl (read_null, F_SETFD, FD_CLOEXEC);
+#endif
+
+      r = posix_spawn_file_actions_adddup2 (&file_actions, read_null, 0);
+      if (r != 0)
+        goto out_close_fds;
+    }
+
+  if (stdout_fd >= 0)
+    {
+      r = posix_spawn_file_actions_adddup2 (&file_actions, stdout_fd, 1);
+      if (r != 0)
+        goto out_close_fds;
+
+      if (!g_slist_find (child_close, GINT_TO_POINTER (stdout_fd)))
+        child_close = g_slist_prepend (child_close, GINT_TO_POINTER (stdout_fd));
+    }
+  else if (stdout_to_null)
+    {
+      gint write_null = safe_open ("/dev/null", O_WRONLY | O_CLOEXEC);
+      g_assert (write_null != -1);
+      parent_close_fds[num_parent_close_fds++] = write_null;
+
+#ifndef HAVE_O_CLOEXEC
+      fcntl (write_null, F_SETFD, FD_CLOEXEC);
+#endif
+
+      r = posix_spawn_file_actions_adddup2 (&file_actions, write_null, 1);
+      if (r != 0)
+        goto out_close_fds;
+    }
+
+  if (stderr_fd >= 0)
+    {
+      r = posix_spawn_file_actions_adddup2 (&file_actions, stderr_fd, 2);
+      if (r != 0)
+        goto out_close_fds;
+
+      if (!g_slist_find (child_close, GINT_TO_POINTER (stderr_fd)))
+        child_close = g_slist_prepend (child_close, GINT_TO_POINTER (stderr_fd));
+    }
+  else if (stderr_to_null)
+    {
+      gint write_null = safe_open ("/dev/null", O_WRONLY | O_CLOEXEC);
+      g_assert (write_null != -1);
+      parent_close_fds[num_parent_close_fds++] = write_null;
+
+#ifndef HAVE_O_CLOEXEC
+      fcntl (write_null, F_SETFD, FD_CLOEXEC);
+#endif
+
+      r = posix_spawn_file_actions_adddup2 (&file_actions, write_null, 2);
+      if (r != 0)
+        goto out_close_fds;
+    }
+
+  /* If source_fds[i] != target_fds[i], we need to handle the case
+   * where the user has specified, e.g., 5 -> 4, 4 -> 6. We do this
+   * by duping the source fds, taking care to ensure the new fds are
+   * larger than any target fd to avoid introducing new conflicts.
+   *
+   * If source_fds[i] == target_fds[i], then we just need to leak
+   * the fd into the child process, which we *could* do by temporarily
+   * unsetting CLOEXEC and then setting it again after we spawn if
+   * it was originally set. POSIX requires that the addup2 action unset
+   * CLOEXEC if source and target are identical, so you'd think doing it
+   * manually wouldn't be needed, but unfortunately as of 2021 many
+   * libcs still don't do so. Example nonconforming libcs:
+   *  Bionic: https://android.googlesource.com/platform/bionic/+/f6e5b582604715729b09db3e36a7aeb8c24b36a4/libc/bionic/spawn.cpp#71
+   *  uclibc-ng: https://cgit.uclibc-ng.org/cgi/cgit/uclibc-ng.git/tree/librt/spawn.c?id=7c36bcae09d66bbaa35cbb02253ae0556f42677e#n88
+   *
+   * Anyway, unsetting CLOEXEC ourselves would open a small race window
+   * where the fd could be inherited into a child process if another
+   * thread spawns something at the same time, because we have not
+   * called fork() and are multithreaded here. This race is avoidable by
+   * using dupfd_cloexec, which we already have to do to handle the
+   * source_fds[i] != target_fds[i] case. So let's always do it!
+   */
+
+  for (i = 0; i < n_fds; i++)
+    max_target_fd = MAX (max_target_fd, target_fds[i]);
+
+  if (max_target_fd == G_MAXINT)
+    goto out_close_fds;
+
+  duped_source_fds = g_new (gint, n_fds);
+  for (i = 0; i < n_fds; i++)
+    {
+      duped_source_fds[i] = dupfd_cloexec (source_fds[i], max_target_fd + 1);
+      if (duped_source_fds[i] < 0)
+        goto out_close_fds;
+    }
+
+  for (i = 0; i < n_fds; i++)
+    {
+      r = posix_spawn_file_actions_adddup2 (&file_actions, duped_source_fds[i], target_fds[i]);
+      if (r != 0)
+        goto out_close_fds;
+    }
+
+  /* Intentionally close the fds in the child as the last file action,
+   * having been careful not to add the same fd to this list twice.
+   *
+   * This is important to allow (e.g.) for the same fd to be passed as stdout
+   * and stderr (we must not close it before we have dupped it in both places,
+   * and we must not attempt to close it twice).
+   */
+  for (elem = child_close; elem != NULL; elem = elem->next)
+    {
+      r = posix_spawn_file_actions_addclose (&file_actions,
+                                             GPOINTER_TO_INT (elem->data));
+      if (r != 0)
+        goto out_close_fds;
+    }
+
+  argv_pass = file_and_argv_zero ? argv + 1 : argv;
+  if (envp == NULL)
+    envp = (const gchar * const *) environ;
+
+  /* Don't search when it contains a slash. */
+  if (!search_path || strchr (argv[0], '/') != NULL)
+    r = posix_spawn (&pid, argv[0], &file_actions, &attr, (char * const *) argv_pass, (char * const *) envp);
+  else
+    r = posix_spawnp (&pid, argv[0], &file_actions, &attr, (char * const *) argv_pass, (char * const *) envp);
+
+  if (r == 0 && child_pid != NULL)
+    *child_pid = pid;
+
+out_close_fds:
+  for (i = 0; i < num_parent_close_fds; i++)
+    close_and_invalidate (&parent_close_fds [i]);
+
+  if (duped_source_fds != NULL)
+    {
+      for (i = 0; i < n_fds; i++)
+        close_and_invalidate (&duped_source_fds[i]);
+      g_free (duped_source_fds);
+    }
+
+  posix_spawn_file_actions_destroy (&file_actions);
+out_free_spawnattr:
+  posix_spawnattr_destroy (&attr);
+  g_slist_free (child_close);
+
+  return r;
+}
+#endif /* POSIX_SPAWN_AVAILABLE */
+
 static gboolean
-fork_exec_with_pipes (gboolean              intermediate_child,
-                      const gchar          *working_directory,
-                      gchar               **argv,
-                      gchar               **envp,
-                      gboolean              close_descriptors,
-                      gboolean              search_path,
-                      gboolean              stdout_to_null,
-                      gboolean              stderr_to_null,
-                      gboolean              child_inherits_stdin,
-                      gboolean              file_and_argv_zero,
-                      GSpawnChildSetupFunc  child_setup,
-                      gpointer              user_data,
-                      GPid                 *child_pid,
-                      gint                 *standard_input,
-                      gint                 *standard_output,
-                      gint                 *standard_error,
-                      GError              **error)     
+fork_exec (gboolean              intermediate_child,
+           const gchar          *working_directory,
+           const gchar * const  *argv,
+           const gchar * const  *envp,
+           gboolean              close_descriptors,
+           gboolean              search_path,
+           gboolean              search_path_from_envp,
+           gboolean              stdout_to_null,
+           gboolean              stderr_to_null,
+           gboolean              child_inherits_stdin,
+           gboolean              file_and_argv_zero,
+           gboolean              cloexec_pipes,
+           GSpawnChildSetupFunc  child_setup,
+           gpointer              user_data,
+           GPid                 *child_pid,
+           gint                 *stdin_pipe_out,
+           gint                 *stdout_pipe_out,
+           gint                 *stderr_pipe_out,
+           gint                  stdin_fd,
+           gint                  stdout_fd,
+           gint                  stderr_fd,
+           const gint           *source_fds,
+           const gint           *target_fds,
+           gsize                 n_fds,
+           GError              **error)
 {
   GPid pid = -1;
-  gint stdin_pipe[2] = { -1, -1 };
-  gint stdout_pipe[2] = { -1, -1 };
-  gint stderr_pipe[2] = { -1, -1 };
   gint child_err_report_pipe[2] = { -1, -1 };
   gint child_pid_report_pipe[2] = { -1, -1 };
+  guint pipe_flags = cloexec_pipes ? FD_CLOEXEC : 0;
   gint status;
-  
-  if (!make_pipe (child_err_report_pipe, error))
-    return FALSE;
+  const gchar *chosen_search_path;
+  gchar *search_path_buffer = NULL;
+  gchar *search_path_buffer_heap = NULL;
+  gsize search_path_buffer_len = 0;
+  gchar **argv_buffer = NULL;
+  gchar **argv_buffer_heap = NULL;
+  gsize argv_buffer_len = 0;
+  gint stdin_pipe[2] = { -1, -1 };
+  gint stdout_pipe[2] = { -1, -1 };
+  gint stderr_pipe[2] = { -1, -1 };
+  gint child_close_fds[4] = { -1, -1, -1, -1 };
+  gint n_child_close_fds = 0;
+  gint *source_fds_copy = NULL;
 
-  if (intermediate_child && !make_pipe (child_pid_report_pipe, error))
-    goto cleanup_and_fail;
-  
-  if (standard_input && !make_pipe (stdin_pipe, error))
-    goto cleanup_and_fail;
-  
-  if (standard_output && !make_pipe (stdout_pipe, error))
-    goto cleanup_and_fail;
+  g_assert (argv != NULL && argv[0] != NULL);
+  g_assert (stdin_pipe_out == NULL || stdin_fd < 0);
+  g_assert (stdout_pipe_out == NULL || stdout_fd < 0);
+  g_assert (stderr_pipe_out == NULL || stderr_fd < 0);
+
+  /* If pipes have been requested, open them */
+  if (stdin_pipe_out != NULL)
+    {
+      if (!g_unix_open_pipe (stdin_pipe, pipe_flags, error))
+        goto cleanup_and_fail;
+      if (_g_spawn_invalid_source_fd (stdin_pipe[0], source_fds, n_fds, error) ||
+          _g_spawn_invalid_source_fd (stdin_pipe[1], source_fds, n_fds, error))
+        goto cleanup_and_fail;
+      child_close_fds[n_child_close_fds++] = stdin_pipe[1];
+      stdin_fd = stdin_pipe[0];
+    }
+
+  if (stdout_pipe_out != NULL)
+    {
+      if (!g_unix_open_pipe (stdout_pipe, pipe_flags, error))
+        goto cleanup_and_fail;
+      if (_g_spawn_invalid_source_fd (stdout_pipe[0], source_fds, n_fds, error) ||
+          _g_spawn_invalid_source_fd (stdout_pipe[1], source_fds, n_fds, error))
+        goto cleanup_and_fail;
+      child_close_fds[n_child_close_fds++] = stdout_pipe[0];
+      stdout_fd = stdout_pipe[1];
+    }
+
+  if (stderr_pipe_out != NULL)
+    {
+      if (!g_unix_open_pipe (stderr_pipe, pipe_flags, error))
+        goto cleanup_and_fail;
+      if (_g_spawn_invalid_source_fd (stderr_pipe[0], source_fds, n_fds, error) ||
+          _g_spawn_invalid_source_fd (stderr_pipe[1], source_fds, n_fds, error))
+        goto cleanup_and_fail;
+      child_close_fds[n_child_close_fds++] = stderr_pipe[0];
+      stderr_fd = stderr_pipe[1];
+    }
+
+  child_close_fds[n_child_close_fds++] = -1;
+
+#ifdef POSIX_SPAWN_AVAILABLE
+  if (!intermediate_child && working_directory == NULL && !close_descriptors &&
+      !search_path_from_envp && child_setup == NULL)
+    {
+      g_trace_mark (G_TRACE_CURRENT_TIME, 0,
+                    "GLib", "posix_spawn",
+                    "%s", argv[0]);
+
+      status = do_posix_spawn (argv,
+                               envp,
+                               search_path,
+                               stdout_to_null,
+                               stderr_to_null,
+                               child_inherits_stdin,
+                               file_and_argv_zero,
+                               child_pid,
+                               child_close_fds,
+                               stdin_fd,
+                               stdout_fd,
+                               stderr_fd,
+                               source_fds,
+                               target_fds,
+                               n_fds);
+      if (status == 0)
+        goto success;
+
+      if (status != ENOEXEC)
+        {
+          g_set_error (error,
+                       G_SPAWN_ERROR,
+                       G_SPAWN_ERROR_FAILED,
+                       _("Failed to spawn child process “%s” (%s)"),
+                       argv[0],
+                       g_strerror (status));
+          goto cleanup_and_fail;
+       }
+
+      /* posix_spawn is not intended to support script execution. It does in
+       * some situations on some glibc versions, but that will be fixed.
+       * So if it fails with ENOEXEC, we fall through to the regular
+       * gspawn codepath so that script execution can be attempted,
+       * per standard gspawn behaviour. */
+      g_debug ("posix_spawn failed (ENOEXEC), fall back to regular gspawn");
+    }
+  else
+    {
+      g_trace_mark (G_TRACE_CURRENT_TIME, 0,
+                    "GLib", "fork",
+                    "posix_spawn avoided %s%s%s%s%s",
+                    !intermediate_child ? "" : "(automatic reaping requested) ",
+                    working_directory == NULL ? "" : "(workdir specified) ",
+                    !close_descriptors ? "" : "(fd close requested) ",
+                    !search_path_from_envp ? "" : "(using envp for search path) ",
+                    child_setup == NULL ? "" : "(child_setup specified) ");
+    }
+#endif /* POSIX_SPAWN_AVAILABLE */
+
+  /* Choose a search path. This has to be done before calling fork()
+   * as getenv() isn’t async-signal-safe (see `man 7 signal-safety`). */
+  chosen_search_path = NULL;
+  if (search_path_from_envp)
+    chosen_search_path = g_environ_getenv ((gchar **) envp, "PATH");
+  if (search_path && chosen_search_path == NULL)
+    chosen_search_path = g_getenv ("PATH");
+
+  if ((search_path || search_path_from_envp) && chosen_search_path == NULL)
+    {
+      /* There is no 'PATH' in the environment.  The default
+       * * search path in libc is the current directory followed by
+       * * the path 'confstr' returns for '_CS_PATH'.
+       * */
+
+      /* In GLib we put . last, for security, and don't use the
+       * * unportable confstr(); UNIX98 does not actually specify
+       * * what to search if PATH is unset. POSIX may, dunno.
+       * */
+
+      chosen_search_path = "/bin:/usr/bin:.";
+    }
+
+  if (search_path || search_path_from_envp)
+    g_assert (chosen_search_path != NULL);
+  else
+    g_assert (chosen_search_path == NULL);
+
+  /* Allocate a buffer which the fork()ed child can use to assemble potential
+   * paths for the binary to exec(), combining the argv[0] and elements from
+   * the chosen_search_path. This can’t be done in the child because malloc()
+   * (or alloca()) are not async-signal-safe (see `man 7 signal-safety`).
+   *
+   * Add 2 for the nul terminator and a leading `/`. */
+  if (chosen_search_path != NULL)
+    {
+      search_path_buffer_len = strlen (chosen_search_path) + strlen (argv[0]) + 2;
+      if (search_path_buffer_len < 4000)
+        {
+          /* Prefer small stack allocations to avoid valgrind leak warnings
+           * in forked child. The 4000B cutoff is arbitrary. */
+          search_path_buffer = g_alloca (search_path_buffer_len);
+        }
+      else
+        {
+          search_path_buffer_heap = g_malloc (search_path_buffer_len);
+          search_path_buffer = search_path_buffer_heap;
+        }
+    }
+
+  if (search_path || search_path_from_envp)
+    g_assert (search_path_buffer != NULL);
+  else
+    g_assert (search_path_buffer == NULL);
+
+  /* And allocate a buffer which is 2 elements longer than @argv, so that if
+   * script_execute() has to be called later on, it can build a wrapper argv
+   * array in this buffer. */
+  argv_buffer_len = g_strv_length ((gchar **) argv) + 2;
+  if (argv_buffer_len < 4000 / sizeof (gchar *))
+    {
+      /* Prefer small stack allocations to avoid valgrind leak warnings
+       * in forked child. The 4000B cutoff is arbitrary. */
+      argv_buffer = g_newa (gchar *, argv_buffer_len);
+    }
+  else
+    {
+      argv_buffer_heap = g_new (gchar *, argv_buffer_len);
+      argv_buffer = argv_buffer_heap;
+    }
+
+  /* And one to hold a copy of @source_fds for later manipulation in do_exec(). */
+  source_fds_copy = g_new (int, n_fds);
+  if (n_fds > 0)
+    memcpy (source_fds_copy, source_fds, sizeof (*source_fds) * n_fds);
 
-  if (standard_error && !make_pipe (stderr_pipe, error))
+  if (!g_unix_open_pipe (child_err_report_pipe, pipe_flags, error))
+    goto cleanup_and_fail;
+  if (_g_spawn_invalid_source_fd (child_err_report_pipe[0], source_fds, n_fds, error) ||
+      _g_spawn_invalid_source_fd (child_err_report_pipe[1], source_fds, n_fds, error))
     goto cleanup_and_fail;
 
+  if (intermediate_child)
+    {
+      if (!g_unix_open_pipe (child_pid_report_pipe, pipe_flags, error))
+        goto cleanup_and_fail;
+      if (_g_spawn_invalid_source_fd (child_pid_report_pipe[0], source_fds, n_fds, error) ||
+          _g_spawn_invalid_source_fd (child_pid_report_pipe[1], source_fds, n_fds, error))
+        goto cleanup_and_fail;
+    }
+  
   pid = fork ();
 
   if (pid < 0)
@@ -1266,9 +2562,12 @@ fork_exec_with_pipes (gboolean              intermediate_child,
        */
       close_and_invalidate (&child_err_report_pipe[0]);
       close_and_invalidate (&child_pid_report_pipe[0]);
-      close_and_invalidate (&stdin_pipe[1]);
-      close_and_invalidate (&stdout_pipe[0]);
-      close_and_invalidate (&stderr_pipe[0]);
+      if (child_close_fds[0] != -1)
+        {
+           int i = -1;
+           while (child_close_fds[++i] != -1)
+             close_and_invalidate (&child_close_fds[i]);
+        }
       
       if (intermediate_child)
         {
@@ -1292,15 +2591,23 @@ fork_exec_with_pipes (gboolean              intermediate_child,
             }
           else if (grandchild_pid == 0)
             {
+              close_and_invalidate (&child_pid_report_pipe[1]);
               do_exec (child_err_report_pipe[1],
-                       stdin_pipe[0],
-                       stdout_pipe[1],
-                       stderr_pipe[1],
+                       stdin_fd,
+                       stdout_fd,
+                       stderr_fd,
+                       source_fds_copy,
+                       target_fds,
+                       n_fds,
                        working_directory,
                        argv,
+                       argv_buffer,
+                       argv_buffer_len,
                        envp,
                        close_descriptors,
-                       search_path,
+                       chosen_search_path,
+                       search_path_buffer,
+                       search_path_buffer_len,
                        stdout_to_null,
                        stderr_to_null,
                        child_inherits_stdin,
@@ -1322,14 +2629,21 @@ fork_exec_with_pipes (gboolean              intermediate_child,
            */
 
           do_exec (child_err_report_pipe[1],
-                   stdin_pipe[0],
-                   stdout_pipe[1],
-                   stderr_pipe[1],
+                   stdin_fd,
+                   stdout_fd,
+                   stderr_fd,
+                   source_fds_copy,
+                   target_fds,
+                   n_fds,
                    working_directory,
                    argv,
+                   argv_buffer,
+                   argv_buffer_len,
                    envp,
                    close_descriptors,
-                   search_path,
+                   chosen_search_path,
+                   search_path_buffer,
+                   search_path_buffer_len,
                    stdout_to_null,
                    stderr_to_null,
                    child_inherits_stdin,
@@ -1348,9 +2662,6 @@ fork_exec_with_pipes (gboolean              intermediate_child,
       /* Close the uncared-about ends of the pipes */
       close_and_invalidate (&child_err_report_pipe[1]);
       close_and_invalidate (&child_pid_report_pipe[1]);
-      close_and_invalidate (&stdin_pipe[0]);
-      close_and_invalidate (&stdout_pipe[1]);
-      close_and_invalidate (&stderr_pipe[1]);
 
       /* If we had an intermediate child, reap it */
       if (intermediate_child)
@@ -1363,8 +2674,7 @@ fork_exec_with_pipes (gboolean              intermediate_child,
               else if (errno == ECHILD)
                 ; /* do nothing, child already reaped */
               else
-                g_warning ("waitpid() should not fail in "
-                          "'fork_exec_with_pipes'");
+                g_warning ("waitpid() should not fail in 'fork_exec'");
             }
         }
       
@@ -1384,7 +2694,7 @@ fork_exec_with_pipes (gboolean              intermediate_child,
               g_set_error (error,
                            G_SPAWN_ERROR,
                            G_SPAWN_ERROR_CHDIR,
-                           _("Failed to change to directory '%s' (%s)"),
+                           _("Failed to change to directory “%s” (%s)"),
                            working_directory,
                            g_strerror (buf[1]));
 
@@ -1393,18 +2703,26 @@ fork_exec_with_pipes (gboolean              intermediate_child,
             case CHILD_EXEC_FAILED:
               g_set_error (error,
                            G_SPAWN_ERROR,
-                           exec_err_to_g_error (buf[1]),
-                           _("Failed to execute child process \"%s\" (%s)"),
+                           _g_spawn_exec_err_to_g_error (buf[1]),
+                           _("Failed to execute child process “%s” (%s)"),
                            argv[0],
                            g_strerror (buf[1]));
 
               break;
-              
-            case CHILD_DUP2_FAILED:
+
+            case CHILD_OPEN_FAILED:
+              g_set_error (error,
+                           G_SPAWN_ERROR,
+                           G_SPAWN_ERROR_FAILED,
+                           _("Failed to open file to remap file descriptor (%s)"),
+                           g_strerror (buf[1]));
+              break;
+
+            case CHILD_DUPFD_FAILED:
               g_set_error (error,
                            G_SPAWN_ERROR,
                            G_SPAWN_ERROR_FAILED,
-                           _("Failed to redirect output or input of child process (%s)"),
+                           _("Failed to duplicate file descriptor for child process (%s)"),
                            g_strerror (buf[1]));
 
               break;
@@ -1416,12 +2734,20 @@ fork_exec_with_pipes (gboolean              intermediate_child,
                            _("Failed to fork child process (%s)"),
                            g_strerror (buf[1]));
               break;
-              
+
+            case CHILD_CLOSE_FAILED:
+              g_set_error (error,
+                           G_SPAWN_ERROR,
+                           G_SPAWN_ERROR_FAILED,
+                           _("Failed to close file descriptor for child process (%s)"),
+                           g_strerror (buf[1]));
+              break;
+
             default:
               g_set_error (error,
                            G_SPAWN_ERROR,
                            G_SPAWN_ERROR_FAILED,
-                           _("Unknown error executing child process \"%s\""),
+                           _("Unknown error executing child process “%s”"),
                            argv[0]);
               break;
             }
@@ -1459,20 +2785,34 @@ fork_exec_with_pipes (gboolean              intermediate_child,
       /* Success against all odds! return the information */
       close_and_invalidate (&child_err_report_pipe[0]);
       close_and_invalidate (&child_pid_report_pipe[0]);
+
+      g_free (search_path_buffer_heap);
+      g_free (argv_buffer_heap);
+      g_free (source_fds_copy);
+
       if (child_pid)
         *child_pid = pid;
 
-      if (standard_input)
-        *standard_input = stdin_pipe[1];
-      if (standard_output)
-        *standard_output = stdout_pipe[0];
-      if (standard_error)
-        *standard_error = stderr_pipe[0];
-      
-      return TRUE;
+      goto success;
     }
 
+success:
+  /* Close the uncared-about ends of the pipes */
+  close_and_invalidate (&stdin_pipe[0]);
+  close_and_invalidate (&stdout_pipe[1]);
+  close_and_invalidate (&stderr_pipe[1]);
+
+  if (stdin_pipe_out != NULL)
+    *stdin_pipe_out = g_steal_fd (&stdin_pipe[1]);
+
+  if (stdout_pipe_out != NULL)
+    *stdout_pipe_out = g_steal_fd (&stdout_pipe[0]);
+
+  if (stderr_pipe_out != NULL)
+    *stderr_pipe_out = g_steal_fd (&stderr_pipe[0]);
+
+  return TRUE;
+
  cleanup_and_fail:
 
   /* There was an error from the Child, reap the child to avoid it being
@@ -1489,15 +2829,10 @@ fork_exec_with_pipes (gboolean              intermediate_child,
           else if (errno == ECHILD)
             ; /* do nothing, child already reaped */
           else
-            g_warning ("waitpid() should not fail in "
-                       "'fork_exec_with_pipes'");
+            g_warning ("waitpid() should not fail in 'fork_exec'");
        }
    }
 
-  close_and_invalidate (&child_err_report_pipe[0]);
-  close_and_invalidate (&child_err_report_pipe[1]);
-  close_and_invalidate (&child_pid_report_pipe[0]);
-  close_and_invalidate (&child_pid_report_pipe[1]);
   close_and_invalidate (&stdin_pipe[0]);
   close_and_invalidate (&stdin_pipe[1]);
   close_and_invalidate (&stdout_pipe[0]);
@@ -1505,64 +2840,57 @@ fork_exec_with_pipes (gboolean              intermediate_child,
   close_and_invalidate (&stderr_pipe[0]);
   close_and_invalidate (&stderr_pipe[1]);
 
-  return FALSE;
-}
+  close_and_invalidate (&child_err_report_pipe[0]);
+  close_and_invalidate (&child_err_report_pipe[1]);
+  close_and_invalidate (&child_pid_report_pipe[0]);
+  close_and_invalidate (&child_pid_report_pipe[1]);
 
-static gboolean
-make_pipe (gint     p[2],
-           GError **error)
-{
-  if (pipe (p) < 0)
-    {
-      gint errsv = errno;
-      g_set_error (error,
-                   G_SPAWN_ERROR,
-                   G_SPAWN_ERROR_FAILED,
-                   _("Failed to create pipe for communicating with child process (%s)"),
-                   g_strerror (errsv));
-      return FALSE;
-    }
-  else
-    return TRUE;
+  g_clear_pointer (&search_path_buffer_heap, g_free);
+  g_clear_pointer (&argv_buffer_heap, g_free);
+  g_clear_pointer (&source_fds_copy, g_free);
+
+  return FALSE;
 }
 
 /* Based on execvp from GNU C Library */
 
-static void
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)) until it calls exec(). */
+static gboolean
 script_execute (const gchar *file,
                 gchar      **argv,
-                gchar      **envp,
-                gboolean     search_path)
+                gchar      **argv_buffer,
+                gsize        argv_buffer_len,
+                gchar      **envp)
 {
   /* Count the arguments.  */
-  int argc = 0;
+  gsize argc = 0;
   while (argv[argc])
     ++argc;
-  
-  /* Construct an argument list for the shell.  */
-  {
-    gchar **new_argv;
 
-    new_argv = g_new0 (gchar*, argc + 2); /* /bin/sh and NULL */
-    
-    new_argv[0] = (char *) "/bin/sh";
-    new_argv[1] = (char *) file;
-    while (argc > 0)
-      {
-       new_argv[argc + 1] = argv[argc];
-       --argc;
-      }
+  /* Construct an argument list for the shell. */
+  if (argc + 2 > argv_buffer_len)
+    return FALSE;
 
-    /* Execute the shell. */
-    if (envp)
-      execve (new_argv[0], new_argv, envp);
-    else
-      execv (new_argv[0], new_argv);
-    
-    g_free (new_argv);
-  }
+  argv_buffer[0] = (char *) "/bin/sh";
+  argv_buffer[1] = (char *) file;
+  while (argc > 0)
+    {
+      argv_buffer[argc + 1] = argv[argc];
+      --argc;
+    }
+
+  /* Execute the shell. */
+  if (envp)
+    execve (argv_buffer[0], argv_buffer, envp);
+  else
+    execv (argv_buffer[0], argv_buffer);
+
+  return TRUE;
 }
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)). */
 static gchar*
 my_strchrnul (const gchar *str, gchar c)
 {
@@ -1573,20 +2901,26 @@ my_strchrnul (const gchar *str, gchar c)
   return p;
 }
 
+/* This function is called between fork() and exec() and hence must be
+ * async-signal-safe (see signal-safety(7)) until it calls exec(). */
 static gint
-g_execute (const gchar *file,
-           gchar      **argv,
-           gchar      **envp,
-           gboolean     search_path)
+g_execute (const gchar  *file,
+           gchar       **argv,
+           gchar       **argv_buffer,
+           gsize         argv_buffer_len,
+           gchar       **envp,
+           const gchar  *search_path,
+           gchar        *search_path_buffer,
+           gsize         search_path_buffer_len)
 {
-  if (*file == '\0')
+  if (file == NULL || *file == '\0')
     {
       /* We check the simple case first. */
       errno = ENOENT;
       return -1;
     }
 
-  if (!search_path || strchr (file, '/') != NULL)
+  if (search_path == NULL || strchr (file, '/') != NULL)
     {
       /* Don't search when it contains a slash. */
       if (envp)
@@ -1594,37 +2928,32 @@ g_execute (const gchar *file,
       else
         execv (file, argv);
       
-      if (errno == ENOEXEC)
-       script_execute (file, argv, envp, FALSE);
+      if (errno == ENOEXEC &&
+          !script_execute (file, argv, argv_buffer, argv_buffer_len, envp))
+        {
+          errno = ENOMEM;
+          return -1;
+        }
     }
   else
     {
       gboolean got_eacces = 0;
       const gchar *path, *p;
-      gchar *name, *freeme;
+      gchar *name;
       gsize len;
       gsize pathlen;
 
-      path = g_getenv ("PATH");
-      if (path == NULL)
-       {
-         /* There is no `PATH' in the environment.  The default
-          * search path in libc is the current directory followed by
-          * the path `confstr' returns for `_CS_PATH'.
-           */
-
-          /* In GLib we put . last, for security, and don't use the
-           * unportable confstr(); UNIX98 does not actually specify
-           * what to search if PATH is unset. POSIX may, dunno.
-           */
-          
-          path = "/bin:/usr/bin:.";
-       }
-
+      path = search_path;
       len = strlen (file) + 1;
       pathlen = strlen (path);
-      freeme = name = g_malloc (pathlen + len + 1);
-      
+      name = search_path_buffer;
+
+      if (search_path_buffer_len < pathlen + len + 1)
+        {
+          errno = ENOMEM;
+          return -1;
+        }
+
       /* Copy the file name at the top, including '\0'  */
       memcpy (name + pathlen + 1, file, len);
       name = name + pathlen;
@@ -1641,7 +2970,7 @@ g_execute (const gchar *file,
 
          if (p == path)
            /* Two adjacent colons, or a colon at the beginning or the end
-             * of `PATH' means to search the current directory.
+             * of 'PATH' means to search the current directory.
              */
            startp = name + 1;
          else
@@ -1653,20 +2982,23 @@ g_execute (const gchar *file,
           else
             execv (startp, argv);
           
-         if (errno == ENOEXEC)
-           script_execute (startp, argv, envp, search_path);
+          if (errno == ENOEXEC &&
+              !script_execute (startp, argv, argv_buffer, argv_buffer_len, envp))
+            {
+              errno = ENOMEM;
+              return -1;
+            }
 
          switch (errno)
            {
            case EACCES:
-             /* Record the we got a `Permission denied' error.  If we end
+             /* Record the we got a 'Permission denied' error.  If we end
                * up finding no executable we can use, we want to diagnose
                * that we did find one but were denied access.
                */
              got_eacces = TRUE;
 
-              /* FALL THRU */
-              
+              G_GNUC_FALLTHROUGH;
            case ENOENT:
 #ifdef ESTALE
            case ESTALE:
@@ -1680,12 +3012,19 @@ g_execute (const gchar *file,
                */
              break;
 
+           case ENODEV:
+           case ETIMEDOUT:
+             /* Some strange filesystems like AFS return even
+              * stranger error numbers.  They cannot reasonably mean anything
+              * else so ignore those, too.
+              */
+             break;
+
            default:
              /* Some other error means we found an executable file, but
                * something went wrong executing it; return the error to our
                * caller.
                */
-              g_free (freeme);
              return -1;
            }
        }
@@ -1697,8 +3036,6 @@ g_execute (const gchar *file,
          * error.
          */
         errno = EACCES;
-
-      g_free (freeme);
     }
 
   /* Return the error from the last attempt (probably ENOENT).  */