]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
support: Add support_spawn_wrap and related functionality
authorFlorian Weimer <fweimer@redhat.com>
Mon, 2 Feb 2026 20:15:48 +0000 (21:15 +0100)
committerFlorian Weimer <fweimer@redhat.com>
Mon, 2 Feb 2026 20:15:48 +0000 (21:15 +0100)
It allows us to write test cases in C that run tests with
dynamic linker wrapping.

The iconv test case was auto-generated.  The posix_spawn usage
is mechanical, and the interface it tests is newly added in this
commit, so this should be acceptable.

Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
support/Makefile
support/subprocess.h
support/support_spawn_wrap.c [new file with mode: 0644]
support/tst-support_spawn_wrap.c [new file with mode: 0644]

index 3a948c7f7e0ad0b25ffefa0ae8b33e5a637d76fd..bcdbe12cfe10cf9affd53332578ae1a0ad9b9d2d 100644 (file)
@@ -91,6 +91,7 @@ libsupport-routines = \
   support_shared_allocate \
   support_small_stack_thread_attribute \
   support_socket_so_timestamp_time64 \
+  support_spawn_wrap \
   support_stack_alloc \
   support_stat_nanoseconds \
   support_subprocess \
@@ -259,6 +260,11 @@ else
 CFLAGS-support_paths.c += -DHARDCODED_PATHS_IN_TEST=false
 endif
 
+CFLAGS-support_spawn_wrap.c += \
+  '-DRUN_PROGRAM_ENV=$(patsubst %, ELEMENT ("%"), $(run-program-env))' \
+  '-DRTLD_PREFIX=$(patsubst %, ELEMENT ("%"), $(rtld-prefix))' \
+  # CFLAGS-support_spawn_wrap.c
+
 # Build with exception handling and asynchronous unwind table support.
 CFLAGS-.oS += -fexceptions -fasynchronous-unwind-tables
 
@@ -352,6 +358,7 @@ tests = \
   tst-support_quote_string \
   tst-support_readdir \
   tst-support_record_failure \
+  tst-support_spawn_wrap \
   tst-test_compare \
   tst-test_compare_blob \
   tst-test_compare_string \
index 99025d99a4fc97b8bb9dcb12734203be8cc497b6..b72ebe090e2ad3035cfed8874596e0de864939a4 100644 (file)
@@ -52,4 +52,36 @@ int support_process_wait (struct support_subprocess *proc);
    then with a SIGKILL.  Return the status as for waitpid call.  */
 int support_process_terminate (struct support_subprocess *proc);
 
+/* Arguments to pass to posix_spawn and related functions to run a
+   process under the built glibc.  This overrides the dynamic linker,
+   its search path, and other search paths, such as for locales.  */
+struct support_spawn_wrapped
+{
+  const char *path;
+  char *const *argv;
+  char *const *envp;
+};
+
+enum support_spawn_wrap_flags
+  {
+    /* Always wrap the invocation, even if test binaries are linked
+       with overridden the default paths to point into the build tree
+       (--enable-hardcoded-path-in-tests).  Can be used to run
+       non-test binaries.  */
+    support_spawn_wrap_force = 1 << 0,
+  };
+
+/* Wrap the invocation for invoking testing.  PATH is the program
+   path.  If ARGV is null, no arguments are passed.  If ENVP is null,
+   environ is used instead.  The result must not be modified.  It is a
+   deep copy of the inputs.  */
+struct support_spawn_wrapped *support_spawn_wrap (const char *path,
+                                                  char *const argv[],
+                                                  char *const envp[],
+                                                  enum
+                                                  support_spawn_wrap_flags);
+
+/* Deallocate the result of support_spawn_wrap.  */
+void support_spawn_wrapped_free (struct support_spawn_wrapped *);
+
 #endif
diff --git a/support/support_spawn_wrap.c b/support/support_spawn_wrap.c
new file mode 100644 (file)
index 0000000..e61e12f
--- /dev/null
@@ -0,0 +1,171 @@
+/* Wrap a subprocess invocation with an ld.so invocation.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <array_length.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/subprocess.h>
+#include <support/support.h>
+#include <unistd.h>
+
+#define ELEMENT(s) s,
+static const char *const rtld_prefix[] = { RTLD_PREFIX "--argv0" };
+static const char *const run_program_env[] = { RUN_PROGRAM_ENV };
+#undef ELEMENT
+
+/* Return a newly allocated argument vector, with ld.so wrapping per
+   rtld_prefix applied if WRAP is true.  */
+static char *const *
+rewrite_argv (const char *path, char *const argv[], bool wrap)
+{
+  char *const substitute[] = { (char *) path, NULL};
+  if (argv == NULL)
+    argv = substitute;
+  TEST_VERIFY (argv[0] != NULL);
+
+  size_t length;
+  for (length = 0; argv[length] != 0; ++length)
+    ;
+  /* Potential wrapping, injected path, and null terminator.  */
+  length += array_length (rtld_prefix) + 1 + 1;
+
+  char **result = xcalloc (length, sizeof (result));
+
+  size_t inpos = 0;
+  size_t outpos = 0;
+  if (wrap)
+    {
+      for (size_t i = 0; i < array_length (rtld_prefix); ++i)
+        {
+          TEST_VERIFY (outpos < length);
+          result[outpos++] = xstrdup (rtld_prefix[i]);
+        }
+
+      /* --argv0 argument.  */
+      TEST_VERIFY (outpos < length);
+      result[outpos++] = xstrdup (argv[0]);
+      inpos = 1;
+
+      /* Path to program as used by ld.so.   */
+      TEST_VERIFY (outpos < length);
+      result[outpos++] = xstrdup (path);
+    }
+
+  for (; argv[inpos] != NULL; ++inpos)
+    {
+      TEST_VERIFY (outpos < length);
+      result[outpos++] = xstrdup (argv[inpos]);
+    }
+
+  TEST_VERIFY (outpos < length);
+  return result;
+
+}
+
+/* Return a newly allocated, rewritten environment, with the settings
+   from run_program_env.  */
+static char *const *
+rewrite_env (char *const envp[])
+{
+  if (envp == NULL)
+    envp = environ;
+
+  size_t length;
+  for (length = 0; envp[length] != 0; ++length)
+    ;
+  length += array_length (run_program_env) + 1;
+
+  /* Set to true if an element of run_program_env is copied.  This is
+     used to avoid adding it again.  */
+  bool copied[array_length (run_program_env)] = { false, };
+
+  char **result = xcalloc (length, sizeof (result));
+  size_t outpos = 0;
+  for (size_t inpos = 0; envp[inpos] != NULL; ++inpos)
+    {
+      const char *to_copy = envp[inpos];
+      /* If there is no assignment operator, this environment string
+         cannot be overridden.  */
+      const char *envp_assign = strchr (to_copy, '=');
+      if (envp_assign != NULL)
+        {
+          size_t length_with_assign = envp_assign - to_copy + 1;
+          for (size_t i = 0; i < array_length (run_program_env); ++i)
+            {
+              if (strncmp (to_copy, run_program_env[i], length_with_assign)
+                  == 0 && !copied[i])
+                {
+                  to_copy = run_program_env[i];
+                  copied[i] = true;
+                  break;
+                }
+            }
+        }
+      TEST_VERIFY (outpos < length);
+      result[outpos++] = xstrdup (to_copy);
+    }
+
+  for (size_t i = 0; i < array_length (run_program_env); ++i)
+    {
+      TEST_VERIFY (strchr (run_program_env[i], '=') != 0);
+      if (!copied[i])
+        {
+          TEST_VERIFY (outpos < length);
+          result[outpos++] = xstrdup (run_program_env[i]);
+        }
+    }
+
+  TEST_VERIFY (outpos < length);
+  return result;
+}
+
+struct support_spawn_wrapped *
+support_spawn_wrap (const char *path,
+                    char *const argv[],
+                    char *const envp[],
+                    enum support_spawn_wrap_flags flags)
+{
+  if (flags != 0)
+    TEST_COMPARE (flags, support_spawn_wrap_force);
+  bool force = flags & support_spawn_wrap_force;
+  bool wrap = force || !support_hardcoded_paths_in_test;
+
+  struct support_spawn_wrapped *result = xmalloc (sizeof (*result));
+  if (wrap)
+    result->path = xstrdup (support_objdir_elf_ldso);
+  else
+    result->path = xstrdup (path);
+  result->argv = rewrite_argv (path, argv, wrap);
+  result->envp = rewrite_env (envp);
+  return result;
+}
+
+void
+support_spawn_wrapped_free (struct support_spawn_wrapped *wrapped)
+{
+  free ((char *) wrapped->path);
+  for (size_t i = 0; wrapped->argv[i] != NULL; ++i)
+    free (wrapped->argv[i]);
+  free ((char **) wrapped->argv);
+  for (size_t i = 0; wrapped->envp[i] != NULL; ++i)
+    free (wrapped->envp[i]);
+  free ((char **) wrapped->envp);
+  free (wrapped);
+}
diff --git a/support/tst-support_spawn_wrap.c b/support/tst-support_spawn_wrap.c
new file mode 100644 (file)
index 0000000..d0bb8a4
--- /dev/null
@@ -0,0 +1,235 @@
+/* Tests for support_spawn_wrap.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <getopt.h>
+#include <spawn.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/subprocess.h>
+#include <support/support.h>
+#include <support/xspawn.h>
+#include <support/xunistd.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/* Return true if running via an explicit ld.so invocation.   */
+static bool
+running_via_ldso (void)
+{
+  char *self = realpath ("/proc/self/exe", NULL);
+  if (self == NULL)
+    FAIL_UNSUPPORTED ("/proc/self/exe not available");
+  char *ldso = realpath (support_objdir_elf_ldso, NULL);
+  TEST_VERIFY_EXIT (ldso != NULL);
+  bool result = strcmp (self, ldso) == 0;
+  free (ldso);
+  free (self);
+  return result;
+}
+
+static void
+test_subprocess (int argc, char **argv)
+{
+  {
+    char *argc_env = getenv ("argc");
+    if (argc_env != NULL)
+      {
+        char *argc_arg = xasprintf ("%d", argc);
+        TEST_COMPARE_STRING (argc_arg, argc_env);
+        free (argc_arg);
+      }
+  }
+
+  if (argc >= 2 && strcmp (argv[1], "alpha") == 0)
+    {
+      TEST_COMPARE (argc, 4);
+      TEST_COMPARE_STRING (argv[0], "program");
+      TEST_COMPARE_STRING (argv[2], "beta");
+      TEST_COMPARE_STRING (argv[3], "gamma");
+      TEST_COMPARE_STRING (argv[4], NULL);
+    }
+  else if (argc >= 2 && strcmp (argv[1], "check-env") == 0)
+    printf ("%d %s\n", argc, getenv ("extra"));
+  else if (argc >= 2 && strcmp (argv[1], "check-ld.so") == 0)
+    TEST_VERIFY (running_via_ldso ());
+}
+
+/* The "recurse" environment variable and the --recurse option
+   indicate recursive invocation.  */
+static int flag_recurse;
+#define CMDLINE_OPTIONS                         \
+  { "recurse", no_argument, &flag_recurse, 1 }, \
+  /* CMDLINE_OPTION */
+
+static void
+prepare (int argc, char **argv)
+{
+  if (getenv ("recurse") != NULL || flag_recurse)
+    {
+      test_subprocess (argc, argv);
+      support_record_failure_barrier ();
+      exit (0);
+    }
+}
+
+#define PREPARE prepare
+
+/* Test wrapping of a non-test program (iconv).  This uses posix_spawn
+   directly, mainly for illustrative purposes.  */
+void
+test_iconv (void)
+{
+  char *iconv_prog = xasprintf ("%s/iconv/iconv_prog", support_objdir_root);
+  char *argv[] = { (char *) "iconv", (char *) "-f", (char *) "UTF-8",
+                   (char *) "-t", (char *) "ISO-8859-1", NULL };
+  struct support_spawn_wrapped *w
+    = support_spawn_wrap (iconv_prog, argv, NULL, support_spawn_wrap_force);
+
+  /* Set up pipes for stdin, stdout, and stderr.  */
+  int stdin_pipe[2];
+  xpipe (stdin_pipe);
+  int stdout_pipe[2];
+  xpipe (stdout_pipe);
+  int stderr_pipe[2];
+  xpipe (stderr_pipe);
+
+  posix_spawn_file_actions_t fa;
+  posix_spawn_file_actions_init (&fa);
+  xposix_spawn_file_actions_adddup2 (&fa, stdin_pipe[0], STDIN_FILENO);
+  xposix_spawn_file_actions_addclose (&fa, stdin_pipe[0]);
+  xposix_spawn_file_actions_addclose (&fa, stdin_pipe[1]);
+  xposix_spawn_file_actions_adddup2 (&fa, stdout_pipe[1], STDOUT_FILENO);
+  xposix_spawn_file_actions_addclose (&fa, stdout_pipe[0]);
+  xposix_spawn_file_actions_addclose (&fa, stdout_pipe[1]);
+  xposix_spawn_file_actions_adddup2 (&fa, stderr_pipe[1], STDERR_FILENO);
+  xposix_spawn_file_actions_addclose (&fa, stderr_pipe[0]);
+  xposix_spawn_file_actions_addclose (&fa, stderr_pipe[1]);
+
+  pid_t pid = xposix_spawn (w->path, &fa, NULL, w->argv, w->envp);
+  posix_spawn_file_actions_destroy (&fa);
+
+  xclose (stdin_pipe[0]);
+  xclose (stdout_pipe[1]);
+  xclose (stderr_pipe[1]);
+
+  /* Write UTF-8 encoding of "äöü\n" to stdin.  */
+  xwrite (stdin_pipe[1], "\xc3\xa4\xc3\xb6\xc3\xbc\n", 7);
+  xclose (stdin_pipe[1]);
+
+  /* Read the converted output from the pipe.  */
+  char buf[16];
+  ssize_t ret = read (stdout_pipe[0], buf, sizeof (buf));
+  xclose (stdout_pipe[0]);
+
+  /* ISO-8859-1 encoding of "äöü\n".  */
+  TEST_COMPARE_BLOB (buf, ret, "\xe4\xf6\xfc\n", 4);
+
+  int status;
+  xwaitpid (pid, &status, 0);
+  TEST_COMPARE (status, 0);
+
+  /* Check that nothing has been written to stderr.  */
+  ret = read (stderr_pipe[0], buf, sizeof (buf));
+  TEST_COMPARE (ret, 0);
+  xclose (stderr_pipe[0]);
+
+  support_spawn_wrapped_free (w);
+  free (iconv_prog);
+}
+
+static int
+do_test (void)
+{
+  char *program = xasprintf ("%s/support/tst-support_spawn_wrap",
+                             support_objdir_root);
+
+  {
+    char *env[] = { (char *) "recurse=", (char *) "argc=1", NULL };
+    struct support_spawn_wrapped *w
+      = support_spawn_wrap (program, NULL, env, 0);
+    struct support_capture_subprocess proc
+      = support_capture_subprogram (w->path, w->argv, w->envp);
+    support_capture_subprocess_check (&proc, "no arguments", 0, sc_allow_none);
+    support_capture_subprocess_free (&proc);
+    support_spawn_wrapped_free (w);
+  }
+
+  {
+    char *argv[] = { (char *) "program", (char *) "--recurse", NULL };
+    struct support_spawn_wrapped *w
+      = support_spawn_wrap (program, argv, NULL, 0);
+    struct support_capture_subprocess proc
+      = support_capture_subprogram (w->path, w->argv, w->envp);
+    support_capture_subprocess_check (&proc, "default envvironment", 0,
+                                      sc_allow_none);
+    support_capture_subprocess_free (&proc);
+    support_spawn_wrapped_free (w);
+  }
+
+  {
+    char *argv[] = { (char *) "program", (char *) "alpha", (char *) "beta",
+                     (char *) "gamma", NULL };
+    char *env[] = { (char *) "recurse=", (char *) "argc=4", NULL };
+    struct support_spawn_wrapped *w
+      = support_spawn_wrap (program, argv, env, 0);
+    struct support_capture_subprocess proc
+      = support_capture_subprogram (w->path, w->argv, w->envp);
+    support_capture_subprocess_check (&proc, "3 arguments", 0, sc_allow_none);
+    support_capture_subprocess_free (&proc);
+    support_spawn_wrapped_free (w);
+  }
+
+  {
+    char *argv[] = { (char *) "program", (char *) "check-env", NULL };
+    char *env[] = { (char *) "recurse=", (char *) "argc=2",
+                    (char *) "extra=17", NULL };
+    struct support_spawn_wrapped *w
+      = support_spawn_wrap (program, argv, env, 0);
+    struct support_capture_subprocess proc
+      = support_capture_subprogram (w->path, w->argv, w->envp);
+    TEST_COMPARE_STRING (proc.out.buffer, "2 17\n");
+    support_capture_subprocess_check (&proc, "check-env", 0, sc_allow_stdout);
+    support_capture_subprocess_free (&proc);
+    support_spawn_wrapped_free (w);
+  }
+
+  test_iconv ();
+
+  /* This may trigger EXIT_UNSUPPORTED, so run this before the tests
+     that rely on running_via_ldso.  */
+  TEST_COMPARE (!running_via_ldso (), support_hardcoded_paths_in_test);
+
+  {
+    char *argv[] = { (char *) "program", (char *) "check-ld.so", NULL };
+    char *env[] = { (char *) "recurse=", NULL };
+    struct support_spawn_wrapped *w
+      = support_spawn_wrap (program, argv, env, support_spawn_wrap_force);
+    struct support_capture_subprocess proc
+      = support_capture_subprogram (w->path, w->argv, w->envp);
+    support_capture_subprocess_check (&proc, "check-ld.so", 0, sc_allow_none);
+    support_capture_subprocess_free (&proc);
+    support_spawn_wrapped_free (w);
+  }
+
+  free (program);
+  return 0;
+}
+
+#include <support/test-driver.c>