[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