From 8e49974e6cb799981805abef5338a10a6f12b514 Mon Sep 17 00:00:00 2001 From: khromenokroman Date: Fri, 30 Oct 2026 12:49:38 +0200 Subject: [PATCH] process-util: add execvp_safe() and execvpe_safe() When execve() fails with ENOEXEC, glibc's execvp() and execvpe() retry the file as a shell script via /bin/sh. Hence a truncated (e.g. empty) binary is run as an empty shell script and exits successfully, instead of failing with ENOEXEC. musl doesn't do this. Let's add variants that look up the file in $PATH via find_executable() and then call execve(), so that ENOEXEC is propagated to the caller. (cherry picked from commit b3ec1fec6d0ec16c08a8f1bb936700cf06f1569d) (cherry picked from commit 68fa520bf57cd2b5a6864bfa7af8c4d6a5563848) --- src/basic/process-util.c | 24 ++++++++++++++++ src/basic/process-util.h | 3 ++ src/test/test-process-util.c | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) 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;