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 b3ec1fec6d)
(cherry picked from commit 68fa520bf5)
This commit is contained in:
khromenokroman authored and Luca Boccassi committed 2026-11-05 14:48:09 +00:00
1 parent 9a7581eeed
commit 8e49974e6c
3 files changed
+80

No files matched your search

+24
View File
@@ -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,
+3
View File
@@ -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,
+53
View File
@@ -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;