The posix_spawn implementation in gnulib comes from glibc as of 2008. But in 2011 an important change was done in glibc: Remove the ability to specify a script that does not start with a '#!' marker as executable. Previously this file was /_assumed_/ to be a shell script.
Here are 3 patches: - Change gnulib's posix_spawn to match what glibc does. - Add unit tests. - Fix the resulting test failures on many platforms (from GNU/Hurd [1] to Solaris 11). [1] https://lists.gnu.org/archive/html/bug-hurd/2020-12/msg00071.html 2020-12-23 Bruno Haible <br...@clisp.org> posix_spawn, posix_spawnp: Fix execution of scripts. * m4/posix_spawn.m4 (gl_POSIX_SPAWN_SECURE): New macro. (gl_POSIX_SPAWN_BODY): Invoke it. Set REPLACE_POSIX_SPAWN if posix_spawn or posix_spawnp allows unsecure execution of scripts. * doc/posix-functions/posix_spawn.texi: Document the script execution problem. * doc/posix-functions/posix_spawnp.texi: Likewise. 2020-12-23 Bruno Haible <br...@clisp.org> Add unit tests regarding execution of scripts. * tests/executable-script: New file. * tests/executable-shell-script: New file. * tests/test-posix_spawn-script.c: New file. * tests/test-posix_spawnp-script.c: New file. * tests/test-execute-script.c: New file. * tests/test-spawn-pipe-script.c: New file. * modules/posix_spawn-tests (Files): Add tests/test-posix_spawn-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-posix_spawn-script. * modules/posix_spawnp-tests (Files): Add tests/test-posix_spawnp-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-posix_spawnp-script. * modules/execute-tests (Files): Add tests/test-execute-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-execute-script. * modules/spawn-pipe-tests (Files): Add tests/test-spawn-pipe-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-spawn-pipe-script. 2020-12-23 Bruno Haible <br...@clisp.org> Don't execute scripts without '#!' marker through /bin/sh. This reflects the change done in glibc through <https://sourceware.org/bugzilla/show_bug.cgi?id=13134> and <https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d96de9634a334af16c0ac711074c15ac1762b23c>. * lib/spawni.c (internal_function): Remove macro. (script_execute): Remove function. (__spawni): Don't invoke script_execute. * lib/execute.c (execute): Disable the ENOEXEC handling. * lib/spawn-pipe.c (create_pipe): Likewise. * NEWS: Mention the change.
>From 9042e0456ac72d9333e2d48970c7f2835acfbf79 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 23 Dec 2020 23:58:17 +0100 Subject: [PATCH 1/3] Don't execute scripts without '#!' marker through /bin/sh. This reflects the change done in glibc through <https://sourceware.org/bugzilla/show_bug.cgi?id=13134> and <https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d96de9634a334af16c0ac711074c15ac1762b23c>. * lib/spawni.c (internal_function): Remove macro. (script_execute): Remove function. (__spawni): Don't invoke script_execute. * lib/execute.c (execute): Disable the ENOEXEC handling. * lib/spawn-pipe.c (create_pipe): Likewise. * NEWS: Mention the change. --- ChangeLog | 13 +++++++++++++ NEWS | 7 +++++++ lib/execute.c | 2 ++ lib/spawn-pipe.c | 4 ++++ lib/spawni.c | 39 --------------------------------------- 5 files changed, 26 insertions(+), 39 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7d38a83..b0d67da 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,18 @@ 2020-12-23 Bruno Haible <br...@clisp.org> + Don't execute scripts without '#!' marker through /bin/sh. + This reflects the change done in glibc through + <https://sourceware.org/bugzilla/show_bug.cgi?id=13134> and + <https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d96de9634a334af16c0ac711074c15ac1762b23c>. + * lib/spawni.c (internal_function): Remove macro. + (script_execute): Remove function. + (__spawni): Don't invoke script_execute. + * lib/execute.c (execute): Disable the ENOEXEC handling. + * lib/spawn-pipe.c (create_pipe): Likewise. + * NEWS: Mention the change. + +2020-12-23 Bruno Haible <br...@clisp.org> + posix_spawn[p]: Fix compilation error on Windows (regr. 2020-12-14). Reported by Adrian Ebeling <d...@adrian-ebeling.de> in <https://lists.gnu.org/archive/html/bug-gnulib/2020-12/msg00189.html>. diff --git a/NEWS b/NEWS index 7679be0..c347ecd 100644 --- a/NEWS +++ b/NEWS @@ -60,6 +60,13 @@ User visible incompatible changes Date Modules Changes +2020-12-23 execute These functions no longer execute scripts without + spawn-pipe '#!' marker through /bin/sh. To execute such a + posix_spawn script as a shell script, either add a '#!/bin/sh' + posix_spawnp marker in the first line, or specify "/bin/sh" as + the program to execute and the script as its first + argument. + 2020-12-18 free This module, obsoleted in 2008, is gone. 2020-12-14 findprog-in The function 'find_in_given_path' now takes a 3rd diff --git a/lib/execute.c b/lib/execute.c index 41a2239..38dd07b 100644 --- a/lib/execute.c +++ b/lib/execute.c @@ -191,6 +191,7 @@ execute (const char *progname, exitcode = spawnpvech (P_WAIT, prog_path, argv + 1, (const char * const *) environ, directory, stdin_handle, stdout_handle, stderr_handle); +# if 0 /* Executing arbitrary files as shell scripts is unsecure. */ if (exitcode == -1 && errno == ENOEXEC) { /* prog is not a native executable. Try to execute it as a @@ -201,6 +202,7 @@ execute (const char *progname, (const char * const *) environ, directory, stdin_handle, stdout_handle, stderr_handle); } +# endif } if (exitcode == -1) saved_errno = errno; diff --git a/lib/spawn-pipe.c b/lib/spawn-pipe.c index d483520..aedbcb2 100644 --- a/lib/spawn-pipe.c +++ b/lib/spawn-pipe.c @@ -287,6 +287,7 @@ create_pipe (const char *progname, child = spawnpvech (P_NOWAIT, prog_path, argv + 1, (const char * const *) environ, directory, stdin_handle, stdout_handle, stderr_handle); +# if 0 /* Executing arbitrary files as shell scripts is unsecure. */ if (child == -1 && errno == ENOEXEC) { /* prog is not a native executable. Try to execute it as a @@ -297,6 +298,7 @@ create_pipe (const char *progname, (const char * const *) environ, directory, stdin_handle, stdout_handle, stderr_handle); } +# endif } failed: if (child == -1) @@ -374,6 +376,7 @@ create_pipe (const char *progname, { child = _spawnvpe (P_NOWAIT, prog_path, argv + 1, (const char **) environ); +# if 0 /* Executing arbitrary files as shell scripts is unsecure. */ if (child == -1 && errno == ENOEXEC) { /* prog is not a native executable. Try to execute it as a @@ -382,6 +385,7 @@ create_pipe (const char *progname, child = _spawnvpe (P_NOWAIT, argv[0], argv, (const char **) environ); } +# endif } if (child == -1) saved_errno = errno; diff --git a/lib/spawni.c b/lib/spawni.c index 06d98b4..182d13f 100644 --- a/lib/spawni.c +++ b/lib/spawni.c @@ -75,9 +75,6 @@ # define sigprocmask __sigprocmask # define strchrnul __strchrnul # define vfork __vfork -#else -# undef internal_function -# define internal_function /* empty */ #endif @@ -105,36 +102,6 @@ __spawni (pid_t *pid, const char *file, #else -/* The file is accessible but it is not an executable file. Invoke - the shell to interpret it as a script. */ -static void -internal_function -script_execute (const char *file, const char *const argv[], - const char *const envp[]) -{ - /* Count the arguments. */ - int argc = 0; - while (argv[argc++]) - ; - - /* Construct an argument list for the shell. */ - { - const char **new_argv = - (const char **) alloca ((argc + 1) * sizeof (const char *)); - new_argv[0] = _PATH_BSHELL; - new_argv[1] = file; - while (argc > 1) - { - new_argv[argc] = argv[argc - 1]; - --argc; - } - - /* Execute the shell. */ - execve (new_argv[0], (char * const *) new_argv, (char * const *) envp); - } -} - - /* Spawn a new process executing PATH with the attributes describes in *ATTRP. Before running the process perform the actions described in FILE-ACTIONS. */ int @@ -307,9 +274,6 @@ __spawni (pid_t *pid, const char *file, /* The FILE parameter is actually a path. */ execve (file, (char * const *) argv, (char * const *) envp); - if (errno == ENOEXEC) - script_execute (file, argv, envp); - /* Oh, oh. 'execve' returns. This is bad. */ _exit (SPAWN_ERROR); } @@ -358,9 +322,6 @@ __spawni (pid_t *pid, const char *file, /* Try to execute this name. If it works, execv will not return. */ execve (startp, (char * const *) argv, (char * const *) envp); - if (errno == ENOEXEC) - script_execute (startp, argv, envp); - switch (errno) { case EACCES: -- 2.7.4
>From 7e9ecfe3790f40ea5b9b18cbfdbb11d0277d27ed Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Thu, 24 Dec 2020 01:14:49 +0100 Subject: [PATCH 2/3] Add unit tests regarding execution of scripts. * tests/executable-script: New file. * tests/executable-shell-script: New file. * tests/test-posix_spawn-script.c: New file. * tests/test-posix_spawnp-script.c: New file. * tests/test-execute-script.c: New file. * tests/test-spawn-pipe-script.c: New file. * modules/posix_spawn-tests (Files): Add tests/test-posix_spawn-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-posix_spawn-script. * modules/posix_spawnp-tests (Files): Add tests/test-posix_spawnp-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-posix_spawnp-script. * modules/execute-tests (Files): Add tests/test-execute-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-execute-script. * modules/spawn-pipe-tests (Files): Add tests/test-spawn-pipe-script.c, tests/executable-script, tests/executable-shell-script. (Makefile.am): Compile and run test-spawn-pipe-script. --- ChangeLog | 24 +++++++ modules/execute-tests | 8 +++ modules/posix_spawn-tests | 10 ++- modules/posix_spawnp-tests | 15 +++- modules/spawn-pipe-tests | 8 +++ tests/executable-script | 4 ++ tests/executable-shell-script | 4 ++ tests/test-execute-script.c | 88 ++++++++++++++++++++++++ tests/test-posix_spawn-script.c | 143 +++++++++++++++++++++++++++++++++++++++ tests/test-posix_spawnp-script.c | 143 +++++++++++++++++++++++++++++++++++++++ tests/test-spawn-pipe-script.c | 100 +++++++++++++++++++++++++++ 11 files changed, 543 insertions(+), 4 deletions(-) create mode 100755 tests/executable-script create mode 100755 tests/executable-shell-script create mode 100644 tests/test-execute-script.c create mode 100644 tests/test-posix_spawn-script.c create mode 100644 tests/test-posix_spawnp-script.c create mode 100644 tests/test-spawn-pipe-script.c diff --git a/ChangeLog b/ChangeLog index b0d67da..bfb7f88 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,29 @@ 2020-12-23 Bruno Haible <br...@clisp.org> + Add unit tests regarding execution of scripts. + * tests/executable-script: New file. + * tests/executable-shell-script: New file. + * tests/test-posix_spawn-script.c: New file. + * tests/test-posix_spawnp-script.c: New file. + * tests/test-execute-script.c: New file. + * tests/test-spawn-pipe-script.c: New file. + * modules/posix_spawn-tests (Files): Add + tests/test-posix_spawn-script.c, tests/executable-script, + tests/executable-shell-script. + (Makefile.am): Compile and run test-posix_spawn-script. + * modules/posix_spawnp-tests (Files): Add + tests/test-posix_spawnp-script.c, tests/executable-script, + tests/executable-shell-script. + (Makefile.am): Compile and run test-posix_spawnp-script. + * modules/execute-tests (Files): Add tests/test-execute-script.c, + tests/executable-script, tests/executable-shell-script. + (Makefile.am): Compile and run test-execute-script. + * modules/spawn-pipe-tests (Files): Add tests/test-spawn-pipe-script.c, + tests/executable-script, tests/executable-shell-script. + (Makefile.am): Compile and run test-spawn-pipe-script. + +2020-12-23 Bruno Haible <br...@clisp.org> + Don't execute scripts without '#!' marker through /bin/sh. This reflects the change done in glibc through <https://sourceware.org/bugzilla/show_bug.cgi?id=13134> and diff --git a/modules/execute-tests b/modules/execute-tests index 9562182..2213a32 100644 --- a/modules/execute-tests +++ b/modules/execute-tests @@ -2,6 +2,9 @@ Files: tests/test-execute.sh tests/test-execute-main.c tests/test-execute-child.c +tests/test-execute-script.c +tests/executable-script +tests/executable-shell-script tests/macros.h Depends-on: @@ -23,3 +26,8 @@ test_execute_main_LDADD = $(LDADD) @LIBINTL@ $(LIBTHREAD) # wrapper script, and should link against as few libraries as possible. # Therefore don't link it against any libraries other than -lc. test_execute_child_LDADD = + +TESTS += test-execute-script +check_PROGRAMS += test-execute-script +test_execute_script_LDADD = $(LDADD) @LIBINTL@ $(LIBTHREAD) +test_execute_script_CPPFLAGS = $(AM_CPPFLAGS) -DSRCDIR=\"$(srcdir)/\" diff --git a/modules/posix_spawn-tests b/modules/posix_spawn-tests index 2c8b4a5..451dbd1 100644 --- a/modules/posix_spawn-tests +++ b/modules/posix_spawn-tests @@ -3,6 +3,9 @@ tests/test-posix_spawn-open1.c tests/test-posix_spawn-open2.c tests/test-posix_spawn-inherit0.c tests/test-posix_spawn-inherit1.c +tests/test-posix_spawn-script.c +tests/executable-script +tests/executable-shell-script tests/signature.h Depends-on: @@ -31,10 +34,13 @@ TESTS += \ test-posix_spawn-open1 \ test-posix_spawn-open2 \ test-posix_spawn-inherit0 \ - test-posix_spawn-inherit1 + test-posix_spawn-inherit1 \ + test-posix_spawn-script check_PROGRAMS += \ test-posix_spawn-open1 \ test-posix_spawn-open2 \ test-posix_spawn-inherit0 \ - test-posix_spawn-inherit1 + test-posix_spawn-inherit1 \ + test-posix_spawn-script +test_posix_spawn_script_CPPFLAGS = $(AM_CPPFLAGS) -DSRCDIR=\"$(srcdir)/\" endif diff --git a/modules/posix_spawnp-tests b/modules/posix_spawnp-tests index 1f6bbc7..04ccdbb 100644 --- a/modules/posix_spawnp-tests +++ b/modules/posix_spawnp-tests @@ -3,6 +3,9 @@ tests/test-posix_spawn-dup2-stdout.c tests/test-posix_spawn-dup2-stdout.in.sh tests/test-posix_spawn-dup2-stdin.c tests/test-posix_spawn-dup2-stdin.in.sh +tests/test-posix_spawnp-script.c +tests/executable-script +tests/executable-shell-script tests/signature.h Depends-on: @@ -35,8 +38,14 @@ AM_CONDITIONAL([POSIX_SPAWN_PORTED], [test $posix_spawn_ported = yes]) Makefile.am: if POSIX_SPAWN_PORTED -TESTS += test-posix_spawn-dup2-stdout test-posix_spawn-dup2-stdin -check_PROGRAMS += test-posix_spawn-dup2-stdout test-posix_spawn-dup2-stdin +TESTS += \ + test-posix_spawn-dup2-stdout \ + test-posix_spawn-dup2-stdin \ + test-posix_spawnp-script +check_PROGRAMS += \ + test-posix_spawn-dup2-stdout \ + test-posix_spawn-dup2-stdin \ + test-posix_spawnp-script BUILT_SOURCES += test-posix_spawn-dup2-stdout.sh test-posix_spawn-dup2-stdout.sh: test-posix_spawn-dup2-stdout.in.sh @@ -51,4 +60,6 @@ test-posix_spawn-dup2-stdin.sh: test-posix_spawn-dup2-stdin.in.sh cp $(srcdir)/test-posix_spawn-dup2-stdin.in.sh $@-t && \ mv $@-t $@ MOSTLYCLEANFILES += test-posix_spawn-dup2-stdin.sh test-posix_spawn-dup2-stdin.sh-t + +test_posix_spawnp_script_CPPFLAGS = $(AM_CPPFLAGS) -DSRCDIR=\"$(srcdir)/\" endif diff --git a/modules/spawn-pipe-tests b/modules/spawn-pipe-tests index 5d1372c..80614a9 100644 --- a/modules/spawn-pipe-tests +++ b/modules/spawn-pipe-tests @@ -2,6 +2,9 @@ Files: tests/test-spawn-pipe.sh tests/test-spawn-pipe-main.c tests/test-spawn-pipe-child.c +tests/test-spawn-pipe-script.c +tests/executable-script +tests/executable-shell-script tests/macros.h Depends-on: @@ -19,3 +22,8 @@ test_spawn_pipe_main_LDADD = $(LDADD) @LIBINTL@ $(LIBTHREAD) # wrapper script, and should link against as few libraries as possible. # Therefore don't link it against any libraries other than -lc. test_spawn_pipe_child_LDADD = + +TESTS += test-spawn-pipe-script +check_PROGRAMS += test-spawn-pipe-script +test_spawn_pipe_script_LDADD = $(LDADD) @LIBINTL@ $(LIBTHREAD) +test_spawn_pipe_script_CPPFLAGS = $(AM_CPPFLAGS) -DSRCDIR=\"$(srcdir)/\" diff --git a/tests/executable-script b/tests/executable-script new file mode 100755 index 0000000..7d8b9cc --- /dev/null +++ b/tests/executable-script @@ -0,0 +1,4 @@ +printf 'Halle ' +printf "Potta" +# This script is intentionally not immediately recognizable as a shell script. +# Don't add a .sh suffix. Don't add a #! header in the first line. diff --git a/tests/executable-shell-script b/tests/executable-shell-script new file mode 100755 index 0000000..1e38117 --- /dev/null +++ b/tests/executable-shell-script @@ -0,0 +1,4 @@ +#!/bin/sh +# This script is a proper shell script. +printf 'Halle ' +printf "Potta" diff --git a/tests/test-execute-script.c b/tests/test-execute-script.c new file mode 100644 index 0000000..060f0c5 --- /dev/null +++ b/tests/test-execute-script.c @@ -0,0 +1,88 @@ +/* Test of execute. + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <https://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include "execute.h" + +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "read-file.h" +#include "macros.h" + +#define DATA_FILENAME "test-execute-script.tmp" + +int +main () +{ + unlink (DATA_FILENAME); + + /* Check an invocation of an executable script. + This should only be supported if the script has a '#!' marker; otherwise + it is unsecure: <https://sourceware.org/bugzilla/show_bug.cgi?id=13134>. + POSIX says that the execlp() and execvp() functions support executing + shell scripts + <https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html>, + but this is considered an antiquated feature. */ + + /* This test is an extension of + "Check stdout is inherited, part 1 (regular file)" + in test-execute-main.c. */ + FILE *fp = freopen (DATA_FILENAME, "w", stdout); + ASSERT (fp != NULL); + + { + const char *progname = "executable-script"; + const char *prog_path = SRCDIR "executable-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int ret = execute (progname, prog_argv[0], prog_argv, NULL, + false, false, false, false, true, false, NULL); + ASSERT (ret == 127); + } + +#if defined _WIN32 && !defined __CYGWIN__ + /* On native Windows, scripts - even with '#!' marker - are not executable. + Only .bat and .cmd files are. */ + ASSERT (fclose (fp) == 0); + ASSERT (unlink (DATA_FILENAME) == 0); + fprintf (stderr, "Skipping test: scripts are not executable on this platform.\n"); + return 77; +#else + { + const char *progname = "executable-shell-script"; + const char *prog_path = SRCDIR "executable-shell-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int ret = execute (progname, prog_argv[0], prog_argv, NULL, + false, false, false, false, true, false, NULL); + ASSERT (ret == 0); + + ASSERT (fclose (fp) == 0); + + size_t length; + char *contents = read_file (DATA_FILENAME, 0, &length); + ASSERT (length == 11 && memcmp (contents, "Halle Potta", 11) == 0); + } + + ASSERT (unlink (DATA_FILENAME) == 0); + + return 0; +#endif +} diff --git a/tests/test-posix_spawn-script.c b/tests/test-posix_spawn-script.c new file mode 100644 index 0000000..a632841 --- /dev/null +++ b/tests/test-posix_spawn-script.c @@ -0,0 +1,143 @@ +/* Test of posix_spawn() function. + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <https://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include <spawn.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include "macros.h" + +#define DATA_FILENAME "test-posix_spawn-script.tmp" + +int +main () +{ + unlink (DATA_FILENAME); + + /* Check an invocation of an executable script. + This should only be supported if the script has a '#!' marker; otherwise + it is unsecure: <https://sourceware.org/bugzilla/show_bug.cgi?id=13134>. + POSIX says that the execlp() and execvp() functions support executing + shell scripts + <https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html>, + but this is considered an antiquated feature. */ + pid_t child; + + posix_spawn_file_actions_t actions; + ASSERT (posix_spawn_file_actions_init (&actions) == 0); + ASSERT (posix_spawn_file_actions_addopen (&actions, STDOUT_FILENO, + DATA_FILENAME, + O_RDWR | O_CREAT | O_TRUNC, 0600) + == 0); + + { + const char *prog_path = SRCDIR "executable-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int err = posix_spawn (&child, prog_path, &actions, NULL, + (char **) prog_argv, environ); + if (err != ENOEXEC) + { + if (err != 0) + { + errno = err; + perror ("posix_spawn"); + return 1; + } + + /* Wait for child. */ + int status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + { + fprintf (stderr, "subprocess terminated with unexpected wait status %d\n", status); + return 1; + } + int exitstatus = WEXITSTATUS (status); + if (exitstatus != 127) + { + fprintf (stderr, "subprocess terminated with unexpected exit status %d\n", exitstatus); + return 1; + } + } + } + + { + const char *prog_path = SRCDIR "executable-shell-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int err = posix_spawn (&child, prog_path, &actions, NULL, + (char **) prog_argv, environ); + if (err != 0) + { + errno = err; + perror ("posix_spawn"); + return 1; + } + + posix_spawn_file_actions_destroy (&actions); + + /* Wait for child. */ + int status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + { + fprintf (stderr, "subprocess terminated with unexpected wait status %d\n", status); + return 1; + } + int exitstatus = WEXITSTATUS (status); + if (exitstatus != 0) + { + fprintf (stderr, "subprocess terminated with unexpected exit status %d\n", exitstatus); + return 1; + } + + /* Check the contents of the data file. */ + FILE *fp = fopen (DATA_FILENAME, "rb"); + if (fp == NULL) + { + perror ("cannot open data file"); + return 1; + } + char buf[1024]; + int nread = fread (buf, 1, sizeof (buf), fp); + if (!(nread == 11 && memcmp (buf, "Halle Potta", 11) == 0)) + { + fprintf (stderr, "data file wrong: has %d bytes, expected %d bytes\n", nread, 11); + return 1; + } + if (fclose (fp)) + { + perror ("cannot close data file"); + return 1; + } + } + + /* Clean up data file. */ + unlink (DATA_FILENAME); + + return 0; +} diff --git a/tests/test-posix_spawnp-script.c b/tests/test-posix_spawnp-script.c new file mode 100644 index 0000000..04bf496 --- /dev/null +++ b/tests/test-posix_spawnp-script.c @@ -0,0 +1,143 @@ +/* Test of posix_spawnp() function. + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <https://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include <spawn.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include "macros.h" + +#define DATA_FILENAME "test-posix_spawn-script.tmp" + +int +main () +{ + unlink (DATA_FILENAME); + + /* Check an invocation of an executable script. + This should only be supported if the script has a '#!' marker; otherwise + it is unsecure: <https://sourceware.org/bugzilla/show_bug.cgi?id=13134>. + POSIX says that the execlp() and execvp() functions support executing + shell scripts + <https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html>, + but this is considered an antiquated feature. */ + pid_t child; + + posix_spawn_file_actions_t actions; + ASSERT (posix_spawn_file_actions_init (&actions) == 0); + ASSERT (posix_spawn_file_actions_addopen (&actions, STDOUT_FILENO, + DATA_FILENAME, + O_RDWR | O_CREAT | O_TRUNC, 0600) + == 0); + + { + const char *prog_path = SRCDIR "executable-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int err = posix_spawnp (&child, prog_path, &actions, NULL, + (char **) prog_argv, environ); + if (err != ENOEXEC) + { + if (err != 0) + { + errno = err; + perror ("posix_spawn"); + return 1; + } + + /* Wait for child. */ + int status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + { + fprintf (stderr, "subprocess terminated with unexpected wait status %d\n", status); + return 1; + } + int exitstatus = WEXITSTATUS (status); + if (exitstatus != 127) + { + fprintf (stderr, "subprocess terminated with unexpected exit status %d\n", exitstatus); + return 1; + } + } + } + + { + const char *prog_path = SRCDIR "executable-shell-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + int err = posix_spawnp (&child, prog_path, &actions, NULL, + (char **) prog_argv, environ); + if (err != 0) + { + errno = err; + perror ("posix_spawn"); + return 1; + } + + posix_spawn_file_actions_destroy (&actions); + + /* Wait for child. */ + int status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + { + fprintf (stderr, "subprocess terminated with unexpected wait status %d\n", status); + return 1; + } + int exitstatus = WEXITSTATUS (status); + if (exitstatus != 0) + { + fprintf (stderr, "subprocess terminated with unexpected exit status %d\n", exitstatus); + return 1; + } + + /* Check the contents of the data file. */ + FILE *fp = fopen (DATA_FILENAME, "rb"); + if (fp == NULL) + { + perror ("cannot open data file"); + return 1; + } + char buf[1024]; + int nread = fread (buf, 1, sizeof (buf), fp); + if (!(nread == 11 && memcmp (buf, "Halle Potta", 11) == 0)) + { + fprintf (stderr, "data file wrong: has %d bytes, expected %d bytes\n", nread, 11); + return 1; + } + if (fclose (fp)) + { + perror ("cannot close data file"); + return 1; + } + } + + /* Clean up data file. */ + unlink (DATA_FILENAME); + + return 0; +} diff --git a/tests/test-spawn-pipe-script.c b/tests/test-spawn-pipe-script.c new file mode 100644 index 0000000..44f9d2c --- /dev/null +++ b/tests/test-spawn-pipe-script.c @@ -0,0 +1,100 @@ +/* Test of create_pipe_in/wait_subprocess. + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <https://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include "spawn-pipe.h" +#include "wait-process.h" + +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "macros.h" + +int +main () +{ + /* Check an invocation of an executable script. + This should only be supported if the script has a '#!' marker; otherwise + it is unsecure: <https://sourceware.org/bugzilla/show_bug.cgi?id=13134>. + POSIX says that the execlp() and execvp() functions support executing + shell scripts + <https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html>, + but this is considered an antiquated feature. */ + int fd[1]; + pid_t pid; + + { + const char *progname = "executable-script"; + const char *prog_path = SRCDIR "executable-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + pid = create_pipe_in (progname, prog_argv[0], prog_argv, NULL, + NULL, false, true, false, fd); + if (pid >= 0) + { + /* Wait for child. */ + ASSERT (wait_subprocess (pid, progname, true, true, true, false, NULL) + == 127); + } + else + { + ASSERT (pid == -1); +#if defined _WIN32 && !defined __CYGWIN__ + ASSERT (errno == ENOENT); +#else + ASSERT (errno == ENOEXEC); +#endif + } + } + +#if defined _WIN32 && !defined __CYGWIN__ + /* On native Windows, scripts - even with '#!' marker - are not executable. + Only .bat and .cmd files are. */ + fprintf (stderr, "Skipping test: scripts are not executable on this platform.\n"); + return 77; +#else + { + const char *progname = "executable-shell-script"; + const char *prog_path = SRCDIR "executable-shell-script"; + const char *prog_argv[2] = { prog_path, NULL }; + + pid = create_pipe_in (progname, prog_argv[0], prog_argv, NULL, + NULL, false, true, false, fd); + ASSERT (pid >= 0); + ASSERT (fd[0] > STDERR_FILENO); + + /* Get child's output. */ + char buffer[1024]; + FILE *fp = fdopen (fd[0], "r"); + ASSERT (fp != NULL); + ASSERT (fread (buffer, 1, sizeof (buffer), fp) == 11); + + /* Check the result. */ + ASSERT (memcmp (buffer, "Halle Potta", 11) == 0); + + /* Wait for child. */ + ASSERT (wait_subprocess (pid, progname, true, false, true, true, NULL) == 0); + + ASSERT (fclose (fp) == 0); + } + + return 0; +#endif +} -- 2.7.4
>From 2845b3bed86ca649d3206d9b1e0fe30a4ca33110 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Thu, 24 Dec 2020 03:49:20 +0100 Subject: [PATCH 3/3] posix_spawn, posix_spawnp: Fix execution of scripts. * m4/posix_spawn.m4 (gl_POSIX_SPAWN_SECURE): New macro. (gl_POSIX_SPAWN_BODY): Invoke it. Set REPLACE_POSIX_SPAWN if posix_spawn or posix_spawnp allows unsecure execution of scripts. * doc/posix-functions/posix_spawn.texi: Document the script execution problem. * doc/posix-functions/posix_spawnp.texi: Likewise. --- ChangeLog | 10 ++ doc/posix-functions/posix_spawn.texi | 5 + doc/posix-functions/posix_spawnp.texi | 5 + m4/posix_spawn.m4 | 169 +++++++++++++++++++++++++++++----- 4 files changed, 164 insertions(+), 25 deletions(-) diff --git a/ChangeLog b/ChangeLog index bfb7f88..e7e5f11 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,15 @@ 2020-12-23 Bruno Haible <br...@clisp.org> + posix_spawn, posix_spawnp: Fix execution of scripts. + * m4/posix_spawn.m4 (gl_POSIX_SPAWN_SECURE): New macro. + (gl_POSIX_SPAWN_BODY): Invoke it. Set REPLACE_POSIX_SPAWN if posix_spawn + or posix_spawnp allows unsecure execution of scripts. + * doc/posix-functions/posix_spawn.texi: Document the script execution + problem. + * doc/posix-functions/posix_spawnp.texi: Likewise. + +2020-12-23 Bruno Haible <br...@clisp.org> + Add unit tests regarding execution of scripts. * tests/executable-script: New file. * tests/executable-shell-script: New file. diff --git a/doc/posix-functions/posix_spawn.texi b/doc/posix-functions/posix_spawn.texi index 7a3a2f6..e0b39b0 100644 --- a/doc/posix-functions/posix_spawn.texi +++ b/doc/posix-functions/posix_spawn.texi @@ -15,6 +15,11 @@ FreeBSD 6.0, NetBSD 5.0, OpenBSD 3.8, Minix 3.1.8, AIX 5.1, HP-UX 11, IRIX 6.5, When this function fails, it causes the stdio buffer contents to be output twice on some platforms: AIX 6.1. +@item +When the program to be invoked is an executable script without a @samp{#!} +marker in the first line, this function executes the script as if it were +a shell script, on some platforms: +GNU/Hurd. @end itemize Portability problems not fixed by Gnulib: diff --git a/doc/posix-functions/posix_spawnp.texi b/doc/posix-functions/posix_spawnp.texi index c420db9..2dd9f68 100644 --- a/doc/posix-functions/posix_spawnp.texi +++ b/doc/posix-functions/posix_spawnp.texi @@ -15,6 +15,11 @@ FreeBSD 6.0, NetBSD 5.0, OpenBSD 3.8, Minix 3.1.8, AIX 5.1, HP-UX 11, IRIX 6.5, When this function fails, it causes the stdio buffer contents to be output twice on some platforms: AIX 6.1. +@item +When the program to be invoked is an executable script without a @samp{#!} +marker in the first line, this function executes the script as if it were +a shell script, on some platforms: +glibc 2.14, GNU/Hurd, Mac OS X 10.13, FreeBSD 12.0, OpenBSD 6.7, AIX 7.2, Solaris 11.4, Cygwin 2.9. @end itemize Portability problems not fixed by Gnulib: diff --git a/m4/posix_spawn.m4 b/m4/posix_spawn.m4 index 625b2ad..59e56fc 100644 --- a/m4/posix_spawn.m4 +++ b/m4/posix_spawn.m4 @@ -1,4 +1,4 @@ -# posix_spawn.m4 serial 18 +# posix_spawn.m4 serial 19 dnl Copyright (C) 2008-2020 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -54,40 +54,52 @@ AC_DEFUN([gl_POSIX_SPAWN_BODY], if test $REPLACE_POSIX_SPAWN = 0; then gl_POSIX_SPAWN_WORKS case "$gl_cv_func_posix_spawn_works" in - *yes) - dnl Assume that these functions are available if POSIX_SPAWN_SETSCHEDULER - dnl evaluates to nonzero. - dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_getschedpolicy]) - dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_setschedpolicy]) - AC_CACHE_CHECK([whether posix_spawnattr_setschedpolicy is supported], - [gl_cv_func_spawnattr_setschedpolicy], - [AC_EGREP_CPP([POSIX scheduling supported], [ + *yes) ;; + *) REPLACE_POSIX_SPAWN=1 ;; + esac + fi + if test $REPLACE_POSIX_SPAWN = 0; then + gl_POSIX_SPAWN_SECURE + case "$gl_cv_func_posix_spawn_secure_exec" in + *yes) ;; + *) REPLACE_POSIX_SPAWN=1 ;; + esac + case "$gl_cv_func_posix_spawnp_secure_exec" in + *yes) ;; + *) REPLACE_POSIX_SPAWN=1 ;; + esac + fi + if test $REPLACE_POSIX_SPAWN = 0; then + dnl Assume that these functions are available if POSIX_SPAWN_SETSCHEDULER + dnl evaluates to nonzero. + dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_getschedpolicy]) + dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_setschedpolicy]) + AC_CACHE_CHECK([whether posix_spawnattr_setschedpolicy is supported], + [gl_cv_func_spawnattr_setschedpolicy], + [AC_EGREP_CPP([POSIX scheduling supported], [ #include <spawn.h> #if POSIX_SPAWN_SETSCHEDULER POSIX scheduling supported #endif ], - [gl_cv_func_spawnattr_setschedpolicy=yes], - [gl_cv_func_spawnattr_setschedpolicy=no]) - ]) - dnl Assume that these functions are available if POSIX_SPAWN_SETSCHEDPARAM - dnl evaluates to nonzero. - dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_getschedparam]) - dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_setschedparam]) - AC_CACHE_CHECK([whether posix_spawnattr_setschedparam is supported], - [gl_cv_func_spawnattr_setschedparam], - [AC_EGREP_CPP([POSIX scheduling supported], [ + [gl_cv_func_spawnattr_setschedpolicy=yes], + [gl_cv_func_spawnattr_setschedpolicy=no]) + ]) + dnl Assume that these functions are available if POSIX_SPAWN_SETSCHEDPARAM + dnl evaluates to nonzero. + dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_getschedparam]) + dnl AC_CHECK_FUNCS_ONCE([posix_spawnattr_setschedparam]) + AC_CACHE_CHECK([whether posix_spawnattr_setschedparam is supported], + [gl_cv_func_spawnattr_setschedparam], + [AC_EGREP_CPP([POSIX scheduling supported], [ #include <spawn.h> #if POSIX_SPAWN_SETSCHEDPARAM POSIX scheduling supported #endif ], - [gl_cv_func_spawnattr_setschedparam=yes], - [gl_cv_func_spawnattr_setschedparam=no]) - ]) - ;; - *) REPLACE_POSIX_SPAWN=1 ;; - esac + [gl_cv_func_spawnattr_setschedparam=yes], + [gl_cv_func_spawnattr_setschedparam=no]) + ]) fi fi if test $ac_cv_func_posix_spawn != yes || test $REPLACE_POSIX_SPAWN = 1; then @@ -417,6 +429,113 @@ main (int argc, char *argv[]) ]) ]) +dnl Test whether posix_spawn and posix_spawnp are secure. +AC_DEFUN([gl_POSIX_SPAWN_SECURE], +[ + AC_REQUIRE([AC_PROG_CC]) + AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles + dnl On many platforms, posix_spawn or posix_spawnp allow executing a + dnl script without a '#!' marker as a shell script. This is unsecure. + AC_CACHE_CHECK([whether posix_spawn rejects scripts without shebang], + [gl_cv_func_posix_spawn_secure_exec], + [echo ':' > conftest.scr + chmod a+x conftest.scr + AC_RUN_IFELSE([AC_LANG_SOURCE([[ + #include <errno.h> + #include <spawn.h> + #include <stddef.h> + #include <sys/types.h> + #include <sys/wait.h> + int + main () + { + const char *prog_path = "./conftest.scr"; + const char *prog_argv[2] = { prog_path, NULL }; + const char *environment[2] = { "PATH=.", NULL }; + pid_t child; + int status; + int err = posix_spawn (&child, prog_path, NULL, NULL, + (char **) prog_argv, (char **) environment); + if (err == ENOEXEC) + return 0; + if (err != 0) + return 1; + status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + return 2; + if (WEXITSTATUS (status) != 127) + return 3; + return 0; + } + ]])], + [gl_cv_func_posix_spawn_secure_exec=yes], + [gl_cv_func_posix_spawn_secure_exec=no], + [case "$host_os" in + # Guess no on GNU/Hurd. + gnu*) + gl_cv_func_posix_spawn_secure_exec="guessing no" ;; + # Guess yes on all other platforms. + *) + gl_cv_func_posix_spawn_secure_exec="guessing yes" ;; + esac + ]) + rm -f conftest.scr + ]) + AC_CACHE_CHECK([whether posix_spawnp rejects scripts without shebang], + [gl_cv_func_posix_spawnp_secure_exec], + [echo ':' > conftest.scr + chmod a+x conftest.scr + AC_RUN_IFELSE([AC_LANG_SOURCE([[ + #include <errno.h> + #include <spawn.h> + #include <stddef.h> + #include <sys/types.h> + #include <sys/wait.h> + int + main () + { + const char *prog_path = "./conftest.scr"; + const char *prog_argv[2] = { prog_path, NULL }; + const char *environment[2] = { "PATH=.", NULL }; + pid_t child; + int status; + int err = posix_spawnp (&child, prog_path, NULL, NULL, + (char **) prog_argv, (char **) environment); + if (err == ENOEXEC) + return 0; + if (err != 0) + return 1; + status = 0; + while (waitpid (child, &status, 0) != child) + ; + if (!WIFEXITED (status)) + return 2; + if (WEXITSTATUS (status) != 127) + return 3; + return 0; + } + ]])], + [gl_cv_func_posix_spawnp_secure_exec=yes], + [gl_cv_func_posix_spawnp_secure_exec=no], + [case "$host_os" in + # Guess yes on glibc systems (glibc >= 2.15 actually) except GNU/Hurd, + # musl libc, NetBSD. + *-gnu* | *-musl* | netbsd*) + gl_cv_func_posix_spawnp_secure_exec="guessing yes" ;; + # Guess no on GNU/Hurd, macOS, FreeBSD, OpenBSD, AIX, Solaris, Cygwin. + gnu* | darwin* | freebsd* | dragonfly* | openbsd* | aix* | solaris* | cygwin*) + gl_cv_func_posix_spawnp_secure_exec="guessing no" ;; + # If we don't know, obey --enable-cross-guesses. + *) + gl_cv_func_posix_spawnp_secure_exec="$gl_cross_guess_normal" ;; + esac + ]) + rm -f conftest.scr + ]) +]) + # Prerequisites of lib/spawni.c. AC_DEFUN([gl_PREREQ_POSIX_SPAWN_INTERNAL], [ -- 2.7.4