[libc-commits] [libc] [libc] Implement execvpe in unistd (PR #234071)
Aman Maurya via libc-commits
libc-commits at lists.llvm.org
Tue Oct 20 21:18:41 PDT 2026
https://github.com/amanmaurya92 created https://github.com/llvm/llvm-project/pull/234071
Implement the standard POSIX.1-2024 function `execvpe` in `<unistd.h>`.
Fixes #234065.
### Details & Specification
Like `execvp`, `execvpe` duplicates the actions of the shell in searching for an executable file if the specified filename does not contain a slash (`/`), resolving against the directories in the caller's `PATH`.
Unlike `execvp`, `execvpe` allows passing an explicit custom environment (`envp`) to the executed process image rather than using the global `environ`.
Per POSIX.1-2024 and Linux specifications:
- `execvpe` searches for the program using the value of `PATH` from the caller's environment, not from the `envp` argument.
- If the filename contains a slash, `PATH` is ignored and `execve` is executed directly with `envp`.
### Changes
- Add `execvpe.h` and `linux/execvpe.cpp` entrypoint.
- Register `execvpe` in `unistd.yaml` and docgen.
- Add entrypoints for `x86_64`, `aarch64`, and `riscv`.
- Add integration tests under `libc/test/integration/src/unistd/execvpe_test.cpp`, including verification that caller `PATH` is prioritized over `envp`.
>From e20b894455d998497253263f2aa8da1f03324f8d Mon Sep 17 00:00:00 2001
From: amanmaurya92 <amanmaurya9209 at gmail.com>
Date: Wed, 21 Oct 2026 09:47:43 +0530
Subject: [PATCH] [libc] Implement execvpe in unistd (#234065)
Implement the standard POSIX.1-2024 function `execvpe` in `<unistd.h>`.
Fixes #234065.
Like `execvp`, `execvpe` duplicates the actions of the shell in searching
for an executable file if the specified filename does not contain a slash (`/`),
resolving against the directories in the caller's `PATH`. Unlike `execvp`,
`execvpe` allows passing an explicit custom environment (`envp`) to the
executed process image rather than using the global `environ`.
Per POSIX.1-2024 and Linux specifications, `execvpe` searches for the program
using the value of `PATH` from the caller's environment, not from the `envp`
argument.
- Add `execvpe.h` and `linux/execvpe.cpp` entrypoint.
- Register `execvpe` in `unistd.yaml` and docgen.
- Add entrypoints for x86_64, aarch64, and riscv.
- Add integration tests under `libc/test/integration/src/unistd/execvpe_test.cpp`,
including verification that caller `PATH` is prioritized over `envp`.
---
libc/config/linux/aarch64/entrypoints.txt | 1 +
libc/config/linux/riscv/entrypoints.txt | 1 +
libc/config/linux/x86_64/entrypoints.txt | 1 +
libc/include/unistd.yaml | 8 +
libc/src/unistd/CMakeLists.txt | 7 +
libc/src/unistd/execvpe.h | 25 +++
libc/src/unistd/linux/CMakeLists.txt | 21 ++
libc/src/unistd/linux/execvpe.cpp | 104 ++++++++++
.../integration/src/unistd/CMakeLists.txt | 20 ++
.../integration/src/unistd/execvpe_test.cpp | 186 ++++++++++++++++++
libc/utils/docgen/unistd.yaml | 2 +
11 files changed, 376 insertions(+)
create mode 100644 libc/src/unistd/execvpe.h
create mode 100644 libc/src/unistd/linux/execvpe.cpp
create mode 100644 libc/test/integration/src/unistd/execvpe_test.cpp
diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index dc806d854c6167..4be30207812136 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1452,6 +1452,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.execl
libc.src.unistd.execv
libc.src.unistd.execvp
+ libc.src.unistd.execvpe
libc.src.unistd.fork
libc.src.unistd.getopt
libc.src.unistd.optarg
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index 1f6df7ce168d3e..cd5b5adb2dbad8 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1651,6 +1651,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.execl
libc.src.unistd.execv
libc.src.unistd.execvp
+ libc.src.unistd.execvpe
libc.src.unistd.fork
libc.src.unistd.getopt
libc.src.unistd.optarg
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index 33b7ec6d9a2813..d4114a0e6c2288 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1677,6 +1677,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.execl
libc.src.unistd.execv
libc.src.unistd.execvp
+ libc.src.unistd.execvpe
libc.src.unistd.fork
libc.src.unistd.getopt
libc.src.unistd.optarg
diff --git a/libc/include/unistd.yaml b/libc/include/unistd.yaml
index ee59a2dc0fe2e4..bde0422e800461 100644
--- a/libc/include/unistd.yaml
+++ b/libc/include/unistd.yaml
@@ -244,6 +244,14 @@ functions:
arguments:
- type: const char *
- type: __exec_argv_t
+ - name: execvpe
+ standards:
+ - posix
+ return_type: int
+ arguments:
+ - type: const char *
+ - type: __exec_argv_t
+ - type: __exec_envp_t
- name: faccessat
standards:
- posix
diff --git a/libc/src/unistd/CMakeLists.txt b/libc/src/unistd/CMakeLists.txt
index 4cbf4d9f26128b..325bcbb8ee67c0 100644
--- a/libc/src/unistd/CMakeLists.txt
+++ b/libc/src/unistd/CMakeLists.txt
@@ -182,6 +182,13 @@ add_entrypoint_object(
.${LIBC_TARGET_OS}.execvp
)
+add_entrypoint_object(
+ execvpe
+ ALIAS
+ DEPENDS
+ .${LIBC_TARGET_OS}.execvpe
+)
+
add_entrypoint_object(
fexecve
ALIAS
diff --git a/libc/src/unistd/execvpe.h b/libc/src/unistd/execvpe.h
new file mode 100644
index 00000000000000..f8f2a514cfe808
--- /dev/null
+++ b/libc/src/unistd/execvpe.h
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation header for execvpe
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_UNISTD_EXECVPE_H
+#define LLVM_LIBC_SRC_UNISTD_EXECVPE_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int execvpe(const char *file, char *const argv[], char *const envp[]);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_UNISTD_EXECVPE_H
diff --git a/libc/src/unistd/linux/CMakeLists.txt b/libc/src/unistd/linux/CMakeLists.txt
index 833fc796054c7c..9304c95ca404cc 100644
--- a/libc/src/unistd/linux/CMakeLists.txt
+++ b/libc/src/unistd/linux/CMakeLists.txt
@@ -286,6 +286,27 @@ add_entrypoint_object(
libc.src.unistd.environ
)
+add_entrypoint_object(
+ execvpe
+ SRCS
+ execvpe.cpp
+ HDRS
+ ../execvpe.h
+ DEPENDS
+ libc.hdr.errno_macros
+ libc.hdr.limits_macros
+ libc.hdr.types.size_t
+ libc.src.__support.common
+ libc.src.__support.CPP.string_view
+ libc.src.__support.libc_errno
+ libc.src.__support.macros.config
+ libc.src.__support.macros.null_check
+ libc.src.__support.OSUtil.linux.syscall_wrappers.execve
+ libc.src.errno.errno
+ libc.src.stdlib.environ_internal
+ libc.src.string.memory_utils.inline_memcpy
+)
+
add_entrypoint_object(
fexecve
SRCS
diff --git a/libc/src/unistd/linux/execvpe.cpp b/libc/src/unistd/linux/execvpe.cpp
new file mode 100644
index 00000000000000..30d4d93b031e66
--- /dev/null
+++ b/libc/src/unistd/linux/execvpe.cpp
@@ -0,0 +1,104 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Linux implementation of execvpe
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/unistd/execvpe.h"
+
+#include "hdr/errno_macros.h"
+#include "hdr/limits_macros.h"
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/execve.h"
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/null_check.h"
+#include "src/stdlib/environ_internal.h"
+#include "src/string/memory_utils/inline_memcpy.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+// TODO: POSIX requires that if execve fails with ENOEXEC, execvpe executes the
+// file using the system shell (as if by `sh -- file args...`). The kernel only
+// handles ELF, "#!" scripts, and binfmt_misc formats, so shebang-less scripts
+// currently fail with ENOEXEC instead of being run by /bin/sh.
+LLVM_LIBC_FUNCTION(int, execvpe,
+ (const char *file, char *const argv[], char *const envp[])) {
+ LIBC_CRASH_ON_NULLPTR(file);
+ LIBC_CRASH_ON_NULLPTR(argv);
+
+ cpp::string_view file_sv(file);
+ if (file_sv.empty()) {
+ libc_errno = ENOENT;
+ return -1;
+ }
+
+ // If there's a / in file, treat it as a path.
+ if (file_sv.find_first_of('/') != cpp::string_view::npos) {
+ auto ret = linux_syscalls::execve(file, argv, envp);
+ if (!ret) {
+ libc_errno = ret.error();
+ return -1;
+ }
+ return *ret;
+ }
+
+ // POSIX.1-2024 / Linux man pages:
+ // execvpe searches for the program using the value of PATH from the caller's
+ // environment, not from the envp argument.
+ constexpr cpp::string_view DEFAULT_PATH = "/bin:/usr/bin";
+ const char *env_path =
+ internal::EnvironmentManager::get_instance().get("PATH");
+ cpp::string_view path_env =
+ env_path != nullptr ? cpp::string_view(env_path) : DEFAULT_PATH;
+
+ char buf[PATH_MAX];
+ bool seen_eacces = false;
+ int last_error = ENOENT;
+
+ for (size_t colon_pos = 0; colon_pos != cpp::string_view::npos;
+ path_env = path_env.substr(colon_pos + 1)) {
+ colon_pos = path_env.find_first_of(':');
+ cpp::string_view dir = path_env.substr(0, colon_pos);
+ if (dir.empty())
+ dir = ".";
+
+ if (dir.size() + 1 + file_sv.size() >= PATH_MAX) {
+ last_error = ENAMETOOLONG;
+ continue;
+ }
+
+ inline_memcpy(buf, dir.data(), dir.size());
+ buf[dir.size()] = '/';
+ inline_memcpy(buf + dir.size() + 1, file_sv.data(), file_sv.size());
+ buf[dir.size() + 1 + file_sv.size()] = '\0';
+
+ auto ret = linux_syscalls::execve(buf, argv, envp);
+ if (ret)
+ return *ret;
+
+ int err = ret.error();
+ if (err == EACCES) {
+ seen_eacces = true;
+ } else if (err == ENOENT || err == ENOTDIR) {
+ last_error = err;
+ } else {
+ libc_errno = err;
+ return -1;
+ }
+ }
+
+ libc_errno = seen_eacces ? EACCES : last_error;
+ return -1;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/test/integration/src/unistd/CMakeLists.txt b/libc/test/integration/src/unistd/CMakeLists.txt
index fa035b86267bc6..15a7b0b1a6e847 100644
--- a/libc/test/integration/src/unistd/CMakeLists.txt
+++ b/libc/test/integration/src/unistd/CMakeLists.txt
@@ -228,6 +228,26 @@ add_integration_test(
EXECV_TEST=PASS
)
+add_integration_test(
+ execvpe_test
+ SUITE
+ unistd-integration-tests
+ SRCS
+ execvpe_test.cpp
+ DEPENDS
+ libc_execv_test_normal_exit
+ libc_execvp_test_normal_exit
+ libc_exec_test_signal_exit
+ libc.hdr.errno_macros
+ libc.hdr.signal_macros
+ libc.hdr.sys_wait_macros
+ libc.src.errno.errno
+ libc.src.stdlib.setenv
+ libc.src.sys.wait.waitpid
+ libc.src.unistd.execvpe
+ libc.src.unistd.fork
+)
+
add_integration_test(
fexecve_test
SUITE
diff --git a/libc/test/integration/src/unistd/execvpe_test.cpp b/libc/test/integration/src/unistd/execvpe_test.cpp
new file mode 100644
index 00000000000000..0e0d8abd1a0e05
--- /dev/null
+++ b/libc/test/integration/src/unistd/execvpe_test.cpp
@@ -0,0 +1,186 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Integration tests for execvpe
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/setenv.h"
+#include "src/sys/wait/waitpid.h"
+#include "src/unistd/execvpe.h"
+#include "src/unistd/fork.h"
+
+#include "hdr/signal_macros.h"
+#include "hdr/sys_wait_macros.h"
+#include "test/IntegrationTest/test.h"
+
+#include <errno.h>
+
+void fork_and_execvpe_slash_path() {
+ pid_t pid = LIBC_NAMESPACE::fork();
+ if (pid == 0) {
+ const char *path = "./libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ char *const envp[] = {
+ const_cast<char *>("EXECV_TEST=PASS"),
+ nullptr,
+ };
+ LIBC_NAMESPACE::execvpe(path, argv, envp);
+ }
+ ASSERT_TRUE(pid > 0);
+ int status;
+ pid_t cpid = LIBC_NAMESPACE::waitpid(pid, &status, 0);
+ ASSERT_TRUE(cpid > 0);
+ ASSERT_EQ(cpid, pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+void fork_and_execvpe_path_search() {
+ pid_t pid = LIBC_NAMESPACE::fork();
+ if (pid == 0) {
+ ASSERT_EQ(LIBC_NAMESPACE::setenv(
+ "PATH", "/nonexistent_dir_for_execvp_test:testdata", 1),
+ 0);
+ const char *file = "libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ char *const envp[] = {
+ const_cast<char *>("EXECV_TEST=PASS"),
+ nullptr,
+ };
+ LIBC_NAMESPACE::execvpe(file, argv, envp);
+ }
+ ASSERT_TRUE(pid > 0);
+ int status;
+ pid_t cpid = LIBC_NAMESPACE::waitpid(pid, &status, 0);
+ ASSERT_TRUE(cpid > 0);
+ ASSERT_EQ(cpid, pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+// POSIX.1-2024 mandates that execvpe() searches for the program using the
+// value of PATH from the caller's environment, not from the envp argument.
+void fork_and_execvpe_caller_path_not_envp() {
+ pid_t pid = LIBC_NAMESPACE::fork();
+ if (pid == 0) {
+ ASSERT_EQ(LIBC_NAMESPACE::setenv("PATH", "testdata", 1), 0);
+ const char *file = "libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ // envp has a bogus PATH, but execvpe must ignore it and use caller's PATH.
+ char *const envp[] = {
+ const_cast<char *>("PATH=/nonexistent_dir_for_execvpe_test"),
+ const_cast<char *>("EXECV_TEST=PASS"),
+ nullptr,
+ };
+ LIBC_NAMESPACE::execvpe(file, argv, envp);
+ }
+ ASSERT_TRUE(pid > 0);
+ int status;
+ pid_t cpid = LIBC_NAMESPACE::waitpid(pid, &status, 0);
+ ASSERT_TRUE(cpid > 0);
+ ASSERT_EQ(cpid, pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+// testdata/noexec contains a file with the same name as the helper binary but
+// with no execute permission bits, so execve on it fails with EACCES.
+void fork_and_execvpe_skips_eacces() {
+ pid_t pid = LIBC_NAMESPACE::fork();
+ if (pid == 0) {
+ ASSERT_EQ(LIBC_NAMESPACE::setenv("PATH", "testdata/noexec:testdata", 1), 0);
+ const char *file = "libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ char *const envp[] = {
+ const_cast<char *>("EXECV_TEST=PASS"),
+ nullptr,
+ };
+ LIBC_NAMESPACE::execvpe(file, argv, envp);
+ }
+ ASSERT_TRUE(pid > 0);
+ int status;
+ pid_t cpid = LIBC_NAMESPACE::waitpid(pid, &status, 0);
+ ASSERT_TRUE(cpid > 0);
+ ASSERT_EQ(cpid, pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+void execvpe_missing_in_path() {
+ ASSERT_EQ(
+ LIBC_NAMESPACE::setenv("PATH", "/nonexistent_dir_for_execvp_test", 1), 0);
+ const char *file = "libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ char *const envp[] = {
+ nullptr,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvpe(file, argv, envp), -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+void execvpe_nonexistent_file() {
+ ASSERT_EQ(LIBC_NAMESPACE::setenv("PATH", "testdata", 1), 0);
+ const char *file = "libc_execvp_nonexistent_file";
+ char *const argv[] = {
+ const_cast<char *>("execvp_nonexistent_file"),
+ nullptr,
+ };
+ char *const envp[] = {
+ nullptr,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvpe(file, argv, envp), -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+void execvpe_reports_eacces() {
+ ASSERT_EQ(LIBC_NAMESPACE::setenv(
+ "PATH", "testdata/noexec:/nonexistent_dir_for_execvp_test", 1),
+ 0);
+ const char *file = "libc_execvp_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execvp_test_normal_exit"),
+ nullptr,
+ };
+ char *const envp[] = {
+ nullptr,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvpe(file, argv, envp), -1);
+ ASSERT_ERRNO_EQ(EACCES);
+}
+
+TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+ [[maybe_unused]] char **envp) {
+ fork_and_execvpe_slash_path();
+ fork_and_execvpe_path_search();
+ fork_and_execvpe_caller_path_not_envp();
+ fork_and_execvpe_skips_eacces();
+ execvpe_missing_in_path();
+ execvpe_nonexistent_file();
+ execvpe_reports_eacces();
+ return 0;
+}
diff --git a/libc/utils/docgen/unistd.yaml b/libc/utils/docgen/unistd.yaml
index d7a976263aa4e0..ec72577de0f6f2 100644
--- a/libc/utils/docgen/unistd.yaml
+++ b/libc/utils/docgen/unistd.yaml
@@ -541,6 +541,8 @@ functions:
in-latest-posix: ""
execvp:
in-latest-posix: ""
+ execvpe:
+ in-latest-posix: ""
faccessat:
in-latest-posix: ""
fchdir:
More information about the libc-commits
mailing list