[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