From: Florian Weimer Date: Mon, 2 Feb 2026 20:15:48 +0000 (+0100) Subject: support: Add support_spawn_wrap and related functionality X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=229f65f5f322609283c7104c80c8af6434dff628;p=thirdparty%2Fglibc.git support: Add support_spawn_wrap and related functionality 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 --- 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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