From ab4e0e2d83ec2b13ce4d3116669e1b60470377d8 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 16 Feb 2025 18:51:40 +0000 Subject: [PATCH] ext/pcntl: pcntl_exec() using execveat whenever possible instead. Allows at least to avoid original process file descriptor to leak in the new process image. Then with Linux 6.14, we can add another layer of security to check beforehand if the executable can be actually safely executed. --- ext/pcntl/config.m4 | 2 +- ext/pcntl/pcntl.c | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/ext/pcntl/config.m4 b/ext/pcntl/config.m4 index ce26a6efd2ead..0ce36712a6f50 100644 --- a/ext/pcntl/config.m4 +++ b/ext/pcntl/config.m4 @@ -10,7 +10,7 @@ if test "$PHP_PCNTL" != "no"; then done AC_CHECK_FUNCS(m4_normalize([ - forkx + execveat getcpuid getpriority pidfd_open diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 8db1d10c698c8..ea7c190e11239 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -39,6 +39,11 @@ #include #endif +#if defined(HAVE_EXECVEAT) +#include +#include +#endif + #ifdef HAVE_WAITID #if defined (HAVE_DECL_P_ALL) && HAVE_DECL_P_ALL == 1 #define HAVE_POSIX_IDTYPES 1 @@ -617,6 +622,40 @@ PHP_FUNCTION(pcntl_wstopsig) } /* }}} */ +#ifdef HAVE_EXECVEAT +static zend_always_inline zend_result php_execve(const char *path, char **argv, char **envp) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return FAILURE; + } +#ifdef AT_EXECVE_CHECK + // Linux >= 6.14 allows to check if `path` is allowed + // for execution per kernel security policy (w/o actually running it) + if (execveat(fd, "", argv, envp, AT_EMPTY_PATH | AT_EXECVE_CHECK) < 0) { + close(fd); + return FAILURE; + } +#endif + if (execveat(fd, "", argv, envp, AT_EMPTY_PATH) < 0) { + close(fd); + return FAILURE; + } + return SUCCESS; +} + +static zend_always_inline zend_result php_execv(const char *path, char **argv) { + return php_execve(path, argv, 0); +} +#else +static zend_always_inline zend_result php_execve(const char *path, char **argv, char **envp) { + return execve(path, argv, envp) == 0 ? SUCCESS : FAILURE; +} + +static zend_always_inline zend_result php_execv(const char *path, char **argv) { + return execv(path, argv) == 0 ? SUCCESS : FAILURE; +} +#endif + /* {{{ Executes specified program in current process space as defined by exec(2) */ PHP_FUNCTION(pcntl_exec) { @@ -716,7 +755,7 @@ PHP_FUNCTION(pcntl_exec) } ZEND_HASH_FOREACH_END(); *(pair) = NULL; - if (execve(path, argv, envp) == -1) { + if (php_execve(path, argv, envp) == FAILURE) { PCNTL_G(last_error) = errno; php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno)); } @@ -727,7 +766,7 @@ PHP_FUNCTION(pcntl_exec) efree(envp); } else { - if (execv(path, argv) == -1) { + if (php_execv(path, argv) == FAILURE) { PCNTL_G(last_error) = errno; php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno)); }