[PATCH v2] support: Add support_spawn_wrap and related functionality
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Mon Feb 2 20:09:34 GMT 2026
On 27/01/26 13:26, Florian Weimer wrote:
> 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.
LGTM, thanks.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
>
> ---
> v2: Address Adhemerval's review comments.
>
> support/Makefile | 7 ++
> support/subprocess.h | 32 ++++++
> support/support_spawn_wrap.c | 171 ++++++++++++++++++++++++++++
> support/tst-support_spawn_wrap.c | 235 +++++++++++++++++++++++++++++++++++++++
> 4 files changed, 445 insertions(+)
>
> diff --git a/support/Makefile b/support/Makefile
> index 3a948c7f7e..bcdbe12cfe 100644
> --- a/support/Makefile
> +++ b/support/Makefile
> @@ -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 \
> diff --git a/support/subprocess.h b/support/subprocess.h
> index 99025d99a4..b72ebe090e 100644
> --- a/support/subprocess.h
> +++ b/support/subprocess.h
> @@ -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
> index 0000000000..e61e12f486
> --- /dev/null
> +++ b/support/support_spawn_wrap.c
> @@ -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
> index 0000000000..d0bb8a4194
> --- /dev/null
> +++ b/support/tst-support_spawn_wrap.c
> @@ -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>
>
> base-commit: 39d9fa9e313308afa44b2b4843f8442e0ae1c067
>
More information about the Libc-alpha
mailing list