Skip to content

Commit d2f7d55

Browse files
committed
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.
1 parent 0d8b139 commit d2f7d55

File tree

2 files changed

+56
-3
lines changed

2 files changed

+56
-3
lines changed

ext/pcntl/config.m4

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ if test "$PHP_PCNTL" != "no"; then
1010
done
1111

1212
AC_CHECK_FUNCS(m4_normalize([
13-
forkx
13+
execveat
1414
getcpuid
1515
getpriority
1616
pidfd_open

ext/pcntl/pcntl.c

+55-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
#include <sys/resource.h>
4040
#endif
4141

42+
#if defined(HAVE_EXECVEAT)
43+
#include <fcntl.h>
44+
#include <unistd.h>
45+
#endif
46+
4247
#ifdef HAVE_WAITID
4348
#if defined (HAVE_DECL_P_ALL) && HAVE_DECL_P_ALL == 1
4449
#define HAVE_POSIX_IDTYPES 1
@@ -617,6 +622,54 @@ PHP_FUNCTION(pcntl_wstopsig)
617622
}
618623
/* }}} */
619624

625+
#ifdef HAVE_EXECVEAT
626+
static zend_always_inline zend_result php_execve(const char *path, char **argv, char **envp) {
627+
int fd = open(path, O_RDONLY | O_CLOEXEC);
628+
if (fd < 0) {
629+
return FAILURE;
630+
}
631+
#ifdef AT_EXECVE_CHECK
632+
// Linux >= 6.14 allows to check if `path` is allowed
633+
// for execution per kernel security policy (w/o actually running it)
634+
if (execveat(fd, "", argv, envp, AT_EMPTY_PATH | AT_EXECVE_CHECK) < 0) {
635+
close(fd);
636+
return FAILURE;
637+
}
638+
#endif
639+
if (execveat(fd, "", argv, envp, AT_EMPTY_PATH) < 0) {
640+
close(fd);
641+
return FAILURE;
642+
}
643+
return SUCCESS;
644+
}
645+
646+
static zend_always_inline zend_result php_execv(const char *path, char **argv) {
647+
int fd = open(path, O_RDONLY | O_CLOEXEC);
648+
if (fd < 0) {
649+
return FAILURE;
650+
}
651+
#ifdef AT_EXECVE_CHECK
652+
if (execveat(fd, "", argv, NULL, AT_EMPTY_PATH | AT_EXECVE_CHECK) < 0) {
653+
close(fd);
654+
return FAILURE;
655+
}
656+
#endif
657+
if (execveat(fd, "", argv, NULL, AT_EMPTY_PATH) < 0) {
658+
close(fd);
659+
return FAILURE;
660+
}
661+
return SUCCESS;
662+
}
663+
#else
664+
static zend_always_inline zend_result php_execve(const char *path, char **argv, char **envp) {
665+
return execve(path, argv, envp) == 0 ? SUCCESS : FAILURE;
666+
}
667+
668+
static zend_always_inline zend_result php_execv(const char *path, char **argv) {
669+
return execv(path, argv) == 0 ? SUCCESS : FAILURE;
670+
}
671+
#endif
672+
620673
/* {{{ Executes specified program in current process space as defined by exec(2) */
621674
PHP_FUNCTION(pcntl_exec)
622675
{
@@ -716,7 +769,7 @@ PHP_FUNCTION(pcntl_exec)
716769
} ZEND_HASH_FOREACH_END();
717770
*(pair) = NULL;
718771

719-
if (execve(path, argv, envp) == -1) {
772+
if (php_execve(path, argv, envp) == FAILURE) {
720773
PCNTL_G(last_error) = errno;
721774
php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno));
722775
}
@@ -727,7 +780,7 @@ PHP_FUNCTION(pcntl_exec)
727780
efree(envp);
728781
} else {
729782

730-
if (execv(path, argv) == -1) {
783+
if (php_execv(path, argv) == FAILURE) {
731784
PCNTL_G(last_error) = errno;
732785
php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno));
733786
}

0 commit comments

Comments
 (0)