Run executable from php without spawning a shell Run executable from php without spawning a shell shell shell

Run executable from php without spawning a shell


To answer your sentence :

Both performance and security wise it's better not to call a shell at all between web server process and executable.

About performances, well, yes, php internals forks, and the shell itself forks too so that's a bit heavy. But you really need to execute a lot of processes to consider those performances issues.

About security, I do not see any issue here. PHP has the escapeshellarg function to sanitize arguments.

The only real problem I met with exec without pcntl is not a resource nor security issue : it is really difficult to create real deamons (without any attachment to its parent, particularily Apache). I solved this by using at, after double-escaping my command:

$arg1 = escapeshellarg($arg1);$arg2 = escapeshellarg($arg2);$command = escapeshellarg("/some/bin $arg1 $arg2 > /dev/null 2>&1 &");exec("$command | at now -M");

To get back to your question, the only way I know to execute programs in a standard (fork+exec) way is to use the PCNTL extension (as already mentionned). Anyway, good luck!


To complete my answer, you can create an exec function yourself that does the same thing as pcntl_fork+pcntl_exec.

I made a my_exec extension that does a classic exec+fork, but actually, I do not think it will solve your issues if you're running this function under apache, because the same behaviour as pcntl_fork will apply (apache2 will be forked and there may be unexpected behaviours with signal catching and so on when execv does not succeed).

config.m4 the phpize configuration file

PHP_ARG_ENABLE(my_exec_extension, whether to enable my extension,[ --enable-my-extension   Enable my extension])if test "$PHP_MY_EXEC_EXTENSION" = "yes"; then  AC_DEFINE(HAVE_MY_EXEC_EXTENSION, 1, [Whether you have my extension])  PHP_NEW_EXTENSION(my_exec_extension, my_exec_extension.c, $ext_shared)fi

my_exec_extension.c the extension

#ifdef HAVE_CONFIG_H#include "config.h"#endif#include "php.h"#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#define PHP_MY_EXEC_EXTENSION_VERSION "1.0"#define PHP_MY_EXEC_EXTENSION_EXTNAME "my_exec_extension"extern zend_module_entry my_exec_extension_module_entry;#define phpext_my_exec_extension_ptr &my_exec_extension_module_entry// declaration of a custom my_exec()PHP_FUNCTION(my_exec);// list of custom PHP functions provided by this extension// set {NULL, NULL, NULL} as the last record to mark the end of liststatic function_entry my_functions[] = {    PHP_FE(my_exec, NULL)    {NULL, NULL, NULL}};// the following code creates an entry for the module and registers it with Zend.zend_module_entry my_exec_extension_module_entry = {#if ZEND_MODULE_API_NO >= 20010901    STANDARD_MODULE_HEADER,#endif    PHP_MY_EXEC_EXTENSION_EXTNAME,    my_functions,    NULL, // name of the MINIT function or NULL if not applicable    NULL, // name of the MSHUTDOWN function or NULL if not applicable    NULL, // name of the RINIT function or NULL if not applicable    NULL, // name of the RSHUTDOWN function or NULL if not applicable    NULL, // name of the MINFO function or NULL if not applicable#if ZEND_MODULE_API_NO >= 20010901    PHP_MY_EXEC_EXTENSION_VERSION,#endif    STANDARD_MODULE_PROPERTIES};ZEND_GET_MODULE(my_exec_extension)char *concat(char *old, char *buf, int buf_len){    int str_size = strlen(old) + buf_len;    char *str = malloc((str_size + 1) * sizeof(char));    snprintf(str, str_size, "%s%s", old, buf);    str[str_size] = '\0';    free(old);    return str;}char *exec_and_return(char *command, char **argv){    int link[2], readlen;    pid_t pid;    char buffer[4096];    char *output;    output = strdup("");    if (pipe(link) < 0)    {        return strdup("Could not pipe!");    }    if ((pid = fork()) < 0)    {        return strdup("Could not fork!");    }    if (pid == 0)    {        dup2(link[1], STDOUT_FILENO);        close(link[0]);        if (execv(command, argv) < 0)        {            printf("Command not found or access denied: %s\n", command);            exit(1);        }    }    else    {        close(link[1]);        while ((readlen = read(link[0], buffer, sizeof(buffer))) > 0)        {            output = concat(output, buffer, readlen);        }        wait(NULL);    }    return output;}PHP_FUNCTION(my_exec){    char *command;    int command_len, argc, i;    zval *arguments, **data;    HashTable *arr_hash;    HashPosition pointer;    char **argv;    // recovers a string (s) and an array (a) from arguments    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &command, &command_len, &arguments) == FAILURE) {        RETURN_NULL();    }    arr_hash = Z_ARRVAL_P(arguments);    // creating argc and argv from our argument array    argc = zend_hash_num_elements(arr_hash);    argv = malloc((argc + 1) * sizeof(char *));    argv[argc] = NULL;    for (            i = 0, zend_hash_internal_pointer_reset_ex(arr_hash, &pointer);            zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS;            zend_hash_move_forward_ex(arr_hash, &pointer)        )    {        if (Z_TYPE_PP(data) == IS_STRING) {            argv[i] = malloc((Z_STRLEN_PP(data) + 1) * sizeof(char));            argv[i][Z_STRLEN_PP(data)] = '\0';            strncpy(argv[i], Z_STRVAL_PP(data), Z_STRLEN_PP(data));            i++;        }    }    char *output = exec_and_return(command, argv);    // freeing allocated memory    for (i = 0; (i < argc); i++)    {        free(argv[i]);    }    free(argv);    // WARNING! I guess there is a memory leak here.    // Second arguemnt to 1 means to PHP: do not free memory    // But if I put 0, I get a segmentation fault    // So I think I do not malloc correctly for a PHP extension.    RETURN_STRING(output, 1);}

test.php a usage sample

<?phpdl("my_exec.so");$output = my_exec("/bin/ls", array("-l", "/"));var_dump($output);

shell script run those commands, of course use your own module directory

phpize./configuremakesudo cp modules/my_exec_extension.so /opt/local/lib/php/extensions/no-debug-non-zts-20090626/my_exec.so

Result

KolyMac:my_fork ninsuo$ php test.phpstring(329) ".DS_Store.Spotlight-V100.Trashes.file.fseventsd.hidden.hotfiles.btree.volAppleScriptApplicationsDeveloperInstaller Log FileLibraryMicrosoft Excel DocumentsMicrosoft Word DocumentsNetworkSystemUsersVolumesbincoresdevetchomelost+foundmach_kernelnetoptprivatesbintmpusrvarvc_command.txtvidotask.txt"

I am not a C dev, so I think there are cleaner ways to achieve this. But you get the idea.


In PHP 7.4+, proc_open open processes directly if cmd is passed as array.

As of PHP 7.4.0, cmd may be passed as array of command parameters. In this case the process will be opened directly (without going through a shell) and PHP will take care of any necessary argument escaping.

So this example:

<?php$file_descriptors = [        0=>['pipe','r'],        1=>['pipe','w'],        2=>['pipe','w']];$cmd_string = 'ps -o comm=';$cmd_array = [        'ps',        '-o',        'comm='];// This is executed by shell:$process = proc_open($cmd_string,$file_descriptors,$pipes);$output = stream_get_contents($pipes[1]);$return = proc_close($process);printf("cmd_string:\n%s\n",$output);// This is executed directly:$process = proc_open($cmd_array,$file_descriptors,$pipes);$output = stream_get_contents($pipes[1]);$return = proc_close($process);printf("cmd_array:\n%s\n",$output);

outputs:

cmd_string:bashphpshpscmd_array:bashphpps