diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 50fdef58b90..a024ee3cd5a 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -2029,6 +2029,30 @@ int make_reaper_process(bool b) { DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(posix_spawnattr_t*, posix_spawnattr_destroy, NULL); +int execvpe_safe(const char *file, char * const *argv, char * const *envp) { + _cleanup_free_ char *resolved = NULL; + int r; + + assert(file); + assert(argv); + assert(envp); + + /* Like execvpe(), but doesn't fall back to running the file via /bin/sh if execve() fails with + * ENOEXEC. Without this a truncated or otherwise broken binary would "successfully" run as an empty + * shell script instead of failing. Returns a negative errno on failure. */ + + r = find_executable(file, &resolved); + if (r < 0) + return r; + + execve(resolved, argv, envp); + return -errno; +} + +int execvp_safe(const char *file, char * const *argv) { + return execvpe_safe(file, argv, environ); +} + int posix_spawn_wrapper( const char *path, char * const *argv, diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 8e55949fa9c..eaceae49f26 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -236,6 +236,9 @@ int get_process_threads(pid_t pid); int is_reaper_process(void); int make_reaper_process(bool b); +int execvpe_safe(const char *file, char * const *argv, char * const *envp); +int execvp_safe(const char *file, char * const *argv); + int posix_spawn_wrapper( const char *path, char * const *argv, diff --git a/src/test/test-process-util.c b/src/test/test-process-util.c index 63458117cb9..92c40ec1964 100644 --- a/src/test/test-process-util.c +++ b/src/test/test-process-util.c @@ -24,21 +24,25 @@ #include "errno-list.h" #include "errno-util.h" #include "fd-util.h" +#include "fs-util.h" #include "ioprio-util.h" #include "log.h" #include "namespace-util.h" #include "parse-util.h" +#include "path-util.h" #include "pidfd-util.h" #include "pidref.h" #include "process-util.h" #include "procfs-util.h" #include "rlimit-util.h" +#include "rm-rf.h" #include "signal-util.h" #include "stdio-util.h" #include "string-util.h" #include "terminal-util.h" #include "tests.h" #include "time-util.h" +#include "tmpfile-util.h" #include "user-util.h" #include "virt.h" @@ -1140,6 +1144,55 @@ TEST(invoked_as) { ASSERT_EQ(unsetenv("SYSTEMD_INVOKED_AS"), 0); } +#define EXIT_EXPECTED_ERROR 42 + +static void test_execvp_safe_one(const char *name, const char *search_path, int expected_error) { + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + int r; + + /* If expected_error is 0, the binary is expected to be executed and to exit successfully. Otherwise + * execvp_safe() is expected to return expected_error. */ + + r = ASSERT_OK(pidref_safe_fork("(execvp-safe)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pidref)); + if (r == 0) { + if (search_path && setenv("PATH", search_path, /* overwrite= */ true) < 0) { + log_error_errno(errno, "Failed to set $PATH: %m"); + _exit(EXIT_FAILURE); + } + + r = execvp_safe(name, STRV_MAKE(name)); + if (r != expected_error) { + log_error_errno(r, "execvp_safe(\"%s\") returned unexpected error: %m", name); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_EXPECTED_ERROR); + } + + int expected_exit_status = expected_error == 0 ? EXIT_SUCCESS : EXIT_EXPECTED_ERROR; + ASSERT_OK_EQ(pidref_wait_for_terminate_and_check("(execvp-safe)", &pidref, WAIT_LOG_ABNORMAL), + expected_exit_status); +} + +TEST(execvp_safe) { + _cleanup_(rm_rf_physical_and_freep) char *d = NULL; + _cleanup_free_ char *p = NULL; + + /* An empty executable file must make execvp_safe() fail with ENOEXEC, and must not be run as an + * empty shell script, which would exit successfully. */ + + ASSERT_OK(mkdtemp_malloc(NULL, &d)); + ASSERT_NOT_NULL(p = path_join(d, "test-process-util-empty")); + ASSERT_OK(touch_file(p, /* parents= */ false, USEC_INFINITY, UID_INVALID, GID_INVALID, 0755)); + if (access(p, X_OK) < 0) /* e.g. /tmp/ mounted noexec */ + return (void) log_tests_skipped_errno(errno, "Cannot execute files in %s", d); + + test_execvp_safe_one(p, /* search_path= */ NULL, -ENOEXEC); + test_execvp_safe_one("test-process-util-empty", d, -ENOEXEC); + test_execvp_safe_one("test-process-util-missing", d, -ENOENT); + test_execvp_safe_one("true", /* search_path= */ NULL, 0); +} + static int intro(void) { log_show_color(true); return EXIT_SUCCESS;