Skip to content

Commit

Permalink
Support proc_open(), exec(), passthru(), system() (#596)
Browse files Browse the repository at this point in the history
## Description

Adds [support for proc_open(), popen($cmd, "w"), exec(), passthru(),
system()](b4cfb8c)
via a custom spawn handler defined in JavaScript via
`php.setSpawnHandler()`.

### `proc_open()`

This PR patches PHP to replace C calls to `proc_open()` with a custom
implementation provided with a new `proc_open7.0.c`/`proc_open7.4.c`
file. The original `proc_open()` relies on POSIX `fork()` function not
available in WebAssembly, so which is why this PR defers the command
execution to the `setSpawnHandler()` callback provided via JavaScript.

### `exec()`, `passthru()`, `system()`

This PR provides a custom `php_exec` implementation (via the
`wasm_php_exec` C function). `php_exec` is the function powering PHP
functions like `exec()`, `passthru()`, `system()`.

### `popen()`

The existing `popen()` implementation is rewired to call the
`setSpawnHandler()` callback. Also, the support for the `popen($cmd,
"w")` mode was added.

### Other notes

This PR removes support for PHP 5.6 as supporting these functions was
challenging there AND also WordPress dropped the support for PHP 5.6.


Closes #594
Closes #710
  • Loading branch information
adamziel authored Oct 24, 2023
1 parent ca83a96 commit 9bceb4a
Show file tree
Hide file tree
Showing 43 changed files with 6,187 additions and 3,513 deletions.
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"prepublishOnly": "npm run build",
"preview": "nx preview playground-website",
"recompile:php:web:light": "nx recompile-php:light php-wasm-web ",
"recompile:php:web:light:5.6": "nx recompile-php:light php-wasm-web --PHP_VERSION=5.6",
"recompile:php:web:light:7.0": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.0",
"recompile:php:web:light:7.1": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.1",
"recompile:php:web:light:7.2": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.2",
Expand All @@ -25,7 +24,6 @@
"recompile:php:web:light:8.1": "nx recompile-php:light php-wasm-web --PHP_VERSION=8.1",
"recompile:php:web:light:8.2": "nx recompile-php:light php-wasm-web --PHP_VERSION=8.2",
"recompile:php:web:kitchen-sink": "nx recompile-php:kitchen-sink php-wasm-web",
"recompile:php:web:kitchen-sink:5.6": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=5.6",
"recompile:php:web:kitchen-sink:7.0": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.0",
"recompile:php:web:kitchen-sink:7.1": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.1",
"recompile:php:web:kitchen-sink:7.2": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.2",
Expand All @@ -35,7 +33,6 @@
"recompile:php:web:kitchen-sink:8.1": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=8.1",
"recompile:php:web:kitchen-sink:8.2": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=8.2",
"recompile:php:node": "nx recompile-php:all php-wasm-node",
"recompile:php:node:5.6": "nx recompile-php php-wasm-node --PHP_VERSION=5.6",
"recompile:php:node:7.0": "nx recompile-php php-wasm-node --PHP_VERSION=7.0",
"recompile:php:node:7.1": "nx recompile-php php-wasm-node --PHP_VERSION=7.1",
"recompile:php:node:7.2": "nx recompile-php php-wasm-node --PHP_VERSION=7.2",
Expand Down
49 changes: 42 additions & 7 deletions packages/php-wasm/cli/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* A CLI script that runs PHP CLI via the WebAssembly build.
*/
import { writeFileSync, existsSync } from 'fs';
import { writeFileSync, existsSync, mkdtempSync } from 'fs';
import { rootCertificates } from 'tls';

import {
Expand All @@ -11,6 +11,7 @@ import {
} from '@php-wasm/universal';

import { NodePHP } from '@php-wasm/node';
import { spawn } from 'child_process';

let args = process.argv.slice(2);
if (!args.length) {
Expand Down Expand Up @@ -40,16 +41,50 @@ const php = await NodePHP.load(phpVersion, {
},
},
});

php.useHostFilesystem();
php.setSpawnHandler((command: string) => {
const phpWasmCommand = `${process.argv[0]} ${process.execArgv.join(' ')} ${
process.argv[1]
}`;
// Naively replace the PHP binary with the PHP-WASM command
// @TODO: Don't process the command. Lean on the shell to do it, e.g. through
// a PATH or an alias.
const updatedCommand = command.replace(
/^(?:\\ |[^ ])*php\d?(\s|$)/,
phpWasmCommand
);

// Create a shell script in a temporary directory
const tempDir = mkdtempSync('php-wasm-');
const tempScriptPath = `${tempDir}/script.sh`;
writeFileSync(
tempScriptPath,
`#!/bin/sh
${updatedCommand} < /dev/stdin
`
);

return spawn('sh', [tempScriptPath], {
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 100,
});
});

const hasMinusCOption = args.some((arg) => arg.startsWith('-c'));
if (!hasMinusCOption) {
args.unshift('-c', defaultPhpIniPath);
}

php.cli(['php', ...args]).catch((result) => {
if (result.name === 'ExitStatus') {
process.exit(result.status === undefined ? 1 : result.status);
}
throw result;
});
await php
.cli(['php', ...args])
.catch((result) => {
if (result.name === 'ExitStatus') {
process.exit(result.status === undefined ? 1 : result.status);
}
throw result;
})
.finally(() => {
process.exit(0);
});
1 change: 1 addition & 0 deletions packages/php-wasm/cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default defineConfig(() => {
'net',
'fs',
'path',
'child_process',
'http',
'tls',
'util',
Expand Down
126 changes: 89 additions & 37 deletions packages/php-wasm/compile/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ FROM emscripten AS emscripten-libzip
ARG PHP_VERSION
COPY --from=emscripten-libz /root/lib /root/lib-libz
RUN /root/copy-lib.sh lib-libz
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
export LIBZIP_VERSION=1.2.0; \
else \
export LIBZIP_VERSION=1.9.2; \
Expand Down Expand Up @@ -334,10 +334,7 @@ COPY --from=emscripten-libzip /root/lib /root/lib-libzip
RUN if [ "$WITH_LIBZIP" = "yes" ]; then \
/root/copy-lib.sh lib-libz; \
/root/copy-lib.sh lib-libzip && \
if [ "${PHP_VERSION:0:1}" -le "5" ]; then \
/root/replace.sh 's/ZEND_MODULE_GLOBALS_CTOR_N/(void (*)(void *))ZEND_MODULE_GLOBALS_CTOR_N/g' /root/php-src/ext/zlib/zlib.c; \
fi;\
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
apt install -y zlib1g zlib1g-dev; \
# https://php-legacy-docs.zend.com/manual/php5/en/zlib.installation
echo -n ' --with-zlib --with-zlib-dir=/root/lib --enable-zip --with-libzip=/root/lib ' >> /root/.php-configure-flags; \
Expand Down Expand Up @@ -390,7 +387,7 @@ RUN if [ "$WITH_LIBXML" = "yes" ]; \
# In the regular cc it's just a warning, but in the emscripten's emcc that's an error:
perl -pi.bak -e 's/char xmlInitParser/void xmlInitParser/g' /root/php-src/configure; \
# On PHP < 7.1.0, the dom_iterators.c file implicitly converts *char to const *char causing emcc error
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "0" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "0" ]]; then \
/root/replace.sh 's/xmlHashScan\(ht, itemHashScanner, iter\);/xmlHashScan(ht, (xmlHashScanner)itemHashScanner, iter);/g' /root/php-src/ext/dom/dom_iterators.c; \
fi; \
else \
Expand Down Expand Up @@ -439,7 +436,7 @@ RUN if [ "$WITH_ICONV" = "yes" ]; \
# PHP <= 7.3 requires Bison 2.7
# PHP >= 7.4 and Bison 3.0
COPY --from=emscripten-bison-2-7 /usr/local/bison /root/linked-bison-27
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
mv /root/linked-bison-27 /usr/local/bison && \
ln -s /usr/local/bison/bin/bison /usr/bin/bison && \
ln -s /usr/local/bison/bin/yacc /usr/bin/yacc; \
Expand Down Expand Up @@ -519,11 +516,14 @@ RUN source /root/emsdk/emsdk_env.sh && \
RUN echo '#define ZEND_MM_ERROR 0' >> /root/php-src/main/php_config.h;

# With HAVE_UNISTD_H=1 PHP complains about the missing getdtablesize() function
RUN /root/replace.sh 's/define php_sleep sleep/define php_sleep wasm_sleep/g' /root/php-src/main/php.h
RUN echo 'extern unsigned int wasm_sleep(unsigned int time);' >> /root/php-src/main/php.h;

RUN /root/replace.sh 's/define HAVE_UNISTD_H 1/define HAVE_UNISTD_H 0/g' /root/php-src/main/php_config.h

# PHP <= 7.3 is not very good at detecting the presence of the POSIX readdir_r function
# so we need to force it to be enabled.
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
echo '#define HAVE_POSIX_READDIR_R 1' >> /root/php-src/main/php_config.h; \
fi;

Expand All @@ -534,15 +534,31 @@ RUN /root/replace.sh 's/static int php_cli_server_poller_poll/extern int wasm_se
# Provide a custom implementation of the php_select() function.
RUN /root/replace.sh 's/return php_select\(/return wasm_select(/g' /root/php-src/sapi/cli/php_cli_server.c

# Provide a custom implementation of the php_exec() function that handles spawning
# the process inside exec(), passthru(), system(), etc.
# We effectively remove the php_exec() implementation from the build by renaming it
# to an unused identifier "php_exec_old", and then we mark php_exec as extern.
RUN /root/replace.sh 's/PHPAPI int php_exec(.+)$/PHPAPI extern int php_exec\1; int php_exec_old\1/g' /root/php-src/ext/standard/exec.c

# Provide a custom implementation of the VCWD_POPEN() function that handles spawning
# the process inside PHP_FUNCTION(popen).
RUN /root/replace.sh 's/VCWD_POPEN\(/wasm_popen(/g' /root/php-src/ext/standard/file.c
RUN /root/replace.sh 's/PHP_FUNCTION\(popen\)/extern FILE *wasm_popen(const char *cmd, const char *mode);PHP_FUNCTION(popen)/g' /root/php-src/ext/standard/file.c

# Provide a custom implementation of the shutdown() function.
RUN perl -pi.bak -e $'s/(\s+)shutdown\(/$1 wasm_shutdown(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN perl -pi.bak -e $'s/(\s+)closesocket\(/$1 wasm_close(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN echo 'extern int wasm_shutdown(int fd, int how);' >> /root/php-src/main/php_config.h;
RUN echo 'extern int wasm_close(int fd);' >> /root/php-src/main/php_config.h;

# Don't ship PHP_FUNCTION(proc_open) with the PHP build
# so that we can ship a patched version with php_wasm.c
RUN echo '' > /root/php-src/ext/standard/proc_open.h;
RUN echo '' > /root/php-src/ext/standard/proc_open.c;

RUN source /root/emsdk/emsdk_env.sh && \
# We're compiling PHP as emscripten's side module...
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dpopen=wasm_popen -Dpclose=wasm_pclose " \
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dphp_exec=wasm_php_exec " \
# ...which means we must skip all the libraries - they will be provided in the final linking step.
EMCC_SKIP="-lz -ledit -ldl -lncurses -lzip -lpng16 -lssl -lcrypto -lxml2 -lc -lm -lsqlite3 /root/lib/lib/libxml2.a /root/lib/lib/libsqlite3.so /root/lib/lib/libsqlite3.a /root/lib/lib/libpng16.so" \
emmake make -j8
Expand All @@ -551,6 +567,16 @@ RUN cp -v /root/php-src/.libs/libphp*.la /root/lib/libphp.la
RUN cp -v /root/php-src/.libs/libphp*.a /root/lib/libphp.a

COPY ./build-assets/php_wasm.c /root/
COPY ./build-assets/proc_open* /root/

RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
cp /root/proc_open7.0.c /root/proc_open.c; \
cp /root/proc_open7.0.h /root/proc_open.h; \
else \
cp /root/proc_open7.4.c /root/proc_open.c; \
cp /root/proc_open7.4.h /root/proc_open.h; \
fi


RUN if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
# Add nodefs when building for node.js
Expand All @@ -574,30 +600,33 @@ RUN if [ "${PHP_VERSION:0:1}" -lt "8" ]; then \
# Add ws networking proxy support if needed
RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
then \
echo -n ' -lwebsocket.js -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-wasm-flags; \
# Emscripten supports yielding from sync functions to JavaScript event loop, but all
# the synchronous functions doing that must be explicitly listed here. This is an
# exhaustive list that was created by compiling PHP with ASYNCIFY, running code that
# uses networking, observing the error, and listing the missing functions.
#
# If you a get an error similar to the one below, you need to add all the function on
# the stack to the "ASYNCIFY_ONLY" list below (in this case, it's php_mysqlnd_net_open_tcp_or_unix_pub):
#
# RuntimeError: unreachable
# at php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[9341]:0x5e42b8)
# at byn$fpcast-emu$php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[17222]:0x7795e9)
# at php_mysqlnd_net_connect_ex_pub (<anonymous>:wasm-function[9338]:0x5e3f02)
#
# Node cuts the trace short by default so use the --stack-trace-limit=50 CLI flag
# to get the entire stack.
#
# -------
#
# Related: Any errors like Fatal error: Cannot redeclare function ...
# are caused by dispatching a PHP request while an execution is paused
# due to an async call – it means the same PHP files are loaded before
# the previous request, where they're already loaded, is concluded.
export ASYNCIFY_IMPORTS=$'["_dlopen_js",\n\
echo -n ' -lwebsocket.js ' >> /root/.emcc-php-wasm-flags; \
fi

RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-wasm-flags; \
# Emscripten supports yielding from sync functions to JavaScript event loop, but all
# the synchronous functions doing that must be explicitly listed here. This is an
# exhaustive list that was created by compiling PHP with ASYNCIFY, running code that
# uses networking, observing the error, and listing the missing functions.
#
# If you a get an error similar to the one below, you need to add all the function on
# the stack to the "ASYNCIFY_ONLY" list below (in this case, it's php_mysqlnd_net_open_tcp_or_unix_pub):
#
# RuntimeError: unreachable
# at php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[9341]:0x5e42b8)
# at byn$fpcast-emu$php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[17222]:0x7795e9)
# at php_mysqlnd_net_connect_ex_pub (<anonymous>:wasm-function[9338]:0x5e3f02)
#
# Node cuts the trace short by default so use the --stack-trace-limit=50 CLI flag
# to get the entire stack.
#
# -------
#
# Related: Any errors like Fatal error: Cannot redeclare function ...
# are caused by dispatching a PHP request while an execution is paused
# due to an async call – it means the same PHP files are loaded before
# the previous request, where they're already loaded, is concluded.
export ASYNCIFY_IMPORTS=$'["_dlopen_js",\n\
"invoke_i",\n\
"invoke_ii",\n\
"invoke_iii",\n\
Expand All @@ -617,9 +646,11 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"invoke_viiiiii",\n\
"invoke_viiiiiii",\n\
"invoke_viiiiiiiii",\n\
"js_open_process",\n\
"js_popen_to_file",\n\
"wasm_poll_socket",\n\
"wasm_shutdown"]'; \
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-wasm-flags; \
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-wasm-flags; \
export ASYNCIFY_ONLY_UNPREFIXED=$'"dynCall_dd",\
"dynCall_i",\
"dynCall_ii",\
Expand Down Expand Up @@ -651,7 +682,14 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"dynCall_viiiii",\
"dynCall_viiiiiii",\
"dynCall_viiiiiiii",'; \
export ASYNCIFY_ONLY=$'"zif_array_filter",\
export ASYNCIFY_ONLY=$'"__fwritex",\
"zif_sleep",\
"zif_stream_get_contents",\
"php_stdiop_read",\
"fwrite",\
"zif_fwrite",\
"php_stdiop_write",\
"zif_array_filter",\
"zend_call_known_instance_method_with_2_params",\
"zend_fetch_dimension_address_read_R",\
"_zval_dtor_func_for_ptr",\
Expand Down Expand Up @@ -707,6 +745,8 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMPVAR_CONST_HANDLER",\
"ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMPVAR_HANDLER",\
"cli",\
"wasm_sleep",\
"wasm_php_exec",\
"wasm_sapi_handle_request",\
"_call_user_function_ex",\
"_call_user_function_impl",\
Expand Down Expand Up @@ -792,6 +832,7 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"php_cli_server_do_event_for_each_fd_callback",\
"php_cli_server_poller_poll",\
"php_cli_server_recv_event_read_request",\
"php_exec",\
"php_execute_script",\
"php_fsockopen_stream",\
"php_getimagesize_from_any",\
Expand Down Expand Up @@ -914,6 +955,15 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"zif_mysqli_stmt_execute",\
"zif_mysqli_stmt_fetch",\
"zif_preg_replace_callback",\
"zif_popen",\
"php_exec_ex",\
"wasm_popen",\
"zif_wasm_popen",\
"zif_system",\
"zif_exec",\
"zif_passthru",\
"zif_shell_exec",\
"zif_proc_open",\
"zif_stream_socket_client",\
"zim_PDOStatement_execute",\
"zim_PDO___construct",\
Expand All @@ -934,8 +984,7 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
if [ "${PHP_VERSION:0:1}" -lt "8" ]; then \
export ASYNCIFY_ONLY="$ASYNCIFY_ONLY,"$(echo "$ASYNCIFY_ONLY" | sed -E $'s/"([a-zA-Z])/"byn$fpcast-emu$\\1/g'); \
fi; \
echo -n ' -s ASYNCIFY_ONLY=['$ASYNCIFY_ONLY_UNPREFIXED$ASYNCIFY_ONLY'] '| tr -d "\n" >> /root/.emcc-php-wasm-flags; \
fi;
echo -n ' -s ASYNCIFY_ONLY=['$ASYNCIFY_ONLY_UNPREFIXED$ASYNCIFY_ONLY'] '| tr -d "\n" >> /root/.emcc-php-wasm-flags;

# Build the final .wasm file
RUN mkdir /root/output
Expand All @@ -945,6 +994,8 @@ RUN source /root/emsdk/emsdk_env.sh && \
"_phpwasm_destroy_uploaded_files_hash", \n\
"_phpwasm_init_uploaded_files_hash", \n\
"_phpwasm_register_uploaded_file", \n\
"_emscripten_sleep", \n\
"_wasm_sleep", \n\
"_wasm_set_phpini_path", \n\
"_wasm_set_phpini_entries", \n\
"_wasm_add_SERVER_entry", \n\
Expand Down Expand Up @@ -984,6 +1035,7 @@ RUN source /root/emsdk/emsdk_env.sh && \
-s INVOKE_RUN=0 \
-s EXIT_RUNTIME=1 \
/root/lib/libphp.a \
/root/proc_open.c \
/root/php_wasm.c \
$(cat /root/.emcc-php-wasm-sources) \
-s ENVIRONMENT=$EMSCRIPTEN_ENVIRONMENT \
Expand Down
Loading

0 comments on commit 9bceb4a

Please sign in to comment.