[libc-commits] [libc] a83ae25 - [libc] implement execvp (#232008)
via libc-commits
libc-commits at lists.llvm.org
Mon Oct 19 10:51:49 PDT 2026
Author: Michael Jones
Date: 2026-10-19T10:51:43-07:00
New Revision: a83ae25f7d923614e33073879d3f5beea53e0592
URL: https://github.com/llvm/llvm-project/commit/a83ae25f7d923614e33073879d3f5beea53e0592
DIFF: https://github.com/llvm/llvm-project/commit/a83ae25f7d923614e33073879d3f5beea53e0592.diff
LOG: [libc] implement execvp (#232008)
Slightly more complex than a normal syscall wrapper. This also needs to
search the PATH.
Described by POSIX:
https://pubs.opengroup.org/onlinepubs/9799919799/functions/exec.html
Assisted-by: Automated tooling, human reviewed.
Added:
libc/src/unistd/execvp.h
libc/src/unistd/linux/execvp.cpp
libc/test/integration/src/unistd/execvp_test.cpp
libc/test/integration/src/unistd/testdata/CMakeLists.txt
libc/test/integration/src/unistd/testdata/execvp_test_normal_exit.cpp
Modified:
libc/config/linux/aarch64/entrypoints.txt
libc/config/linux/riscv/entrypoints.txt
libc/config/linux/x86_64/entrypoints.txt
libc/include/unistd.yaml
libc/src/unistd/CMakeLists.txt
libc/src/unistd/linux/CMakeLists.txt
libc/test/integration/src/unistd/CMakeLists.txt
Removed:
################################################################################
diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index aed29d806f2bf..f19427f507b03 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1449,6 +1449,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.environ
libc.src.unistd.execl
libc.src.unistd.execv
+ libc.src.unistd.execvp
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 85377eda33ffc..f9156b9fd6e4a 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1648,6 +1648,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.environ
libc.src.unistd.execl
libc.src.unistd.execv
+ libc.src.unistd.execvp
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 6022e4951b673..7125b2248a875 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1674,6 +1674,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.unistd.environ
libc.src.unistd.execl
libc.src.unistd.execv
+ libc.src.unistd.execvp
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 5964e35b6ce5d..a94bf195a4eef 100644
--- a/libc/include/unistd.yaml
+++ b/libc/include/unistd.yaml
@@ -237,6 +237,13 @@ functions:
- type: const char *
- type: __exec_argv_t
- type: __exec_envp_t
+ - name: execvp
+ standards:
+ - posix
+ return_type: int
+ arguments:
+ - type: const char *
+ - type: __exec_argv_t
- name: faccessat
standards:
- posix
diff --git a/libc/src/unistd/CMakeLists.txt b/libc/src/unistd/CMakeLists.txt
index 27b74b8ca2de3..aee321f599588 100644
--- a/libc/src/unistd/CMakeLists.txt
+++ b/libc/src/unistd/CMakeLists.txt
@@ -175,6 +175,13 @@ add_entrypoint_object(
.${LIBC_TARGET_OS}.execve
)
+add_entrypoint_object(
+ execvp
+ ALIAS
+ DEPENDS
+ .${LIBC_TARGET_OS}.execvp
+)
+
add_entrypoint_object(
fexecve
ALIAS
diff --git a/libc/src/unistd/execvp.h b/libc/src/unistd/execvp.h
new file mode 100644
index 0000000000000..a52dfa2abfad1
--- /dev/null
+++ b/libc/src/unistd/execvp.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 execvp
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_UNISTD_EXECVP_H
+#define LLVM_LIBC_SRC_UNISTD_EXECVP_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int execvp(const char *file, char *const argv[]);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_UNISTD_EXECVP_H
diff --git a/libc/src/unistd/linux/CMakeLists.txt b/libc/src/unistd/linux/CMakeLists.txt
index 61030ec7e1228..4a24077f4f4c7 100644
--- a/libc/src/unistd/linux/CMakeLists.txt
+++ b/libc/src/unistd/linux/CMakeLists.txt
@@ -264,6 +264,28 @@ add_entrypoint_object(
libc.src.errno.errno
)
+add_entrypoint_object(
+ execvp
+ SRCS
+ execvp.cpp
+ HDRS
+ ../execvp.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
+ libc.src.unistd.environ
+)
+
add_entrypoint_object(
fexecve
SRCS
diff --git a/libc/src/unistd/linux/execvp.cpp b/libc/src/unistd/linux/execvp.cpp
new file mode 100644
index 0000000000000..9b64e7c9a4bec
--- /dev/null
+++ b/libc/src/unistd/linux/execvp.cpp
@@ -0,0 +1,105 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 execvp
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/unistd/execvp.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"
+#include "src/unistd/environ.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+// TODO: POSIX requires that if execve fails with ENOEXEC, execvp 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, execvp, (const char *file, char *const argv[])) {
+ 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, const_cast<char *const *>(LIBC_NAMESPACE::environ));
+ if (!ret) {
+ libc_errno = ret.error();
+ return -1;
+ }
+ return *ret;
+ }
+
+ // else treat file as the name of a file in a directory, search the
+ // directories listed in PATH.
+ 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, const_cast<char *const *>(LIBC_NAMESPACE::environ));
+ 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 cc04b374a8378..fa035b86267bc 100644
--- a/libc/test/integration/src/unistd/CMakeLists.txt
+++ b/libc/test/integration/src/unistd/CMakeLists.txt
@@ -1,6 +1,8 @@
add_custom_target(unistd-integration-tests)
add_dependencies(libc-integration-tests unistd-integration-tests)
+add_subdirectory(testdata)
+
add_integration_test(
getcwd_test
SUITE
@@ -204,6 +206,28 @@ add_integration_test(
EXECV_TEST=PASS
)
+add_integration_test(
+ execvp_test
+ SUITE
+ unistd-integration-tests
+ SRCS
+ execvp_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.execvp
+ libc.src.unistd.fork
+ ENV
+ EXECV_TEST=PASS
+)
+
add_integration_test(
fexecve_test
SUITE
diff --git a/libc/test/integration/src/unistd/execvp_test.cpp b/libc/test/integration/src/unistd/execvp_test.cpp
new file mode 100644
index 0000000000000..ef027044e03bd
--- /dev/null
+++ b/libc/test/integration/src/unistd/execvp_test.cpp
@@ -0,0 +1,136 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 execvp
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/setenv.h"
+#include "src/sys/wait/waitpid.h"
+#include "src/unistd/execvp.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_execvp_slash_path() {
+ pid_t pid = LIBC_NAMESPACE::fork();
+ if (pid == 0) {
+ const char *path = "./libc_execv_test_normal_exit";
+ char *const argv[] = {
+ const_cast<char *>("execv_test_normal_exit"),
+ nullptr,
+ };
+ LIBC_NAMESPACE::execvp(path, argv);
+ }
+ 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_execvp_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,
+ };
+ LIBC_NAMESPACE::execvp(file, argv);
+ }
+ 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 execvp_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,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvp(file, argv), -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+void execvp_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,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvp(file, argv), -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+// 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_execvp_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,
+ };
+ LIBC_NAMESPACE::execvp(file, argv);
+ }
+ 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 execvp_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,
+ };
+ errno = 0;
+ ASSERT_EQ(LIBC_NAMESPACE::execvp(file, argv), -1);
+ ASSERT_ERRNO_EQ(EACCES);
+}
+
+TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+ [[maybe_unused]] char **envp) {
+ fork_and_execvp_slash_path();
+ fork_and_execvp_path_search();
+ fork_and_execvp_skips_eacces();
+ execvp_missing_in_path();
+ execvp_nonexistent_file();
+ execvp_reports_eacces();
+ return 0;
+}
diff --git a/libc/test/integration/src/unistd/testdata/CMakeLists.txt b/libc/test/integration/src/unistd/testdata/CMakeLists.txt
new file mode 100644
index 0000000000000..3fcce1d885669
--- /dev/null
+++ b/libc/test/integration/src/unistd/testdata/CMakeLists.txt
@@ -0,0 +1,20 @@
+add_executable(
+ libc_execvp_test_normal_exit
+ EXCLUDE_FROM_ALL
+ execvp_test_normal_exit.cpp
+)
+set_target_properties(
+ libc_execvp_test_normal_exit
+ PROPERTIES
+ OUTPUT_NAME libc_execvp_test_normal_exit
+ RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+# A file with the same name as the helper binary but without any execute
+# permission bits. Used to test that execvp continues searching PATH after
+# EACCES and reports EACCES if no later PATH entry succeeds.
+set(execvp_noexec_file
+ ${CMAKE_CURRENT_BINARY_DIR}/noexec/libc_execvp_test_normal_exit)
+file(WRITE ${execvp_noexec_file} "#!/bin/sh\nexit 1\n")
+file(CHMOD ${execvp_noexec_file}
+ PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ)
diff --git a/libc/test/integration/src/unistd/testdata/execvp_test_normal_exit.cpp b/libc/test/integration/src/unistd/testdata/execvp_test_normal_exit.cpp
new file mode 100644
index 0000000000000..8b162485a36b5
--- /dev/null
+++ b/libc/test/integration/src/unistd/testdata/execvp_test_normal_exit.cpp
@@ -0,0 +1,23 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Helper test program for execvp integration tests
+///
+//===----------------------------------------------------------------------===//
+
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main() {
+ char *env = getenv("EXECV_TEST");
+ if (env == nullptr)
+ raise(SIGUSR1);
+ return 0;
+}
More information about the libc-commits
mailing list