close
Skip to content

[PHP] Call php.mount instead of php.FS.mount in proxyFileSystem#3346

Merged
adamziel merged 10 commits intotrunkfrom
fix/proc-open-multiple-calls-v2
Mar 9, 2026
Merged

[PHP] Call php.mount instead of php.FS.mount in proxyFileSystem#3346
adamziel merged 10 commits intotrunkfrom
fix/proc-open-multiple-calls-v2

Conversation

@adamziel
Copy link
Collaborator

@adamziel adamziel commented Mar 5, 2026

Fixes proc_open('php -r "..."') silently returning empty output on the 2nd and subsequent calls.

Implementation

The output was empty as the rotated runtime lost access to the site's filesystem. Every non-first PHP runtime mounts the document root and some /internal directories from the primary PHP's filesystem. That is done in the proxyFileSystem() call. Now, this mounting operation called php.FS.mount() which directly created a mounted resource in the Emscripten Filesystem. It should have used php.mount() instead.

At the end of the CLI call, the PHP runtime is invalidated and marked for rotation. During the rotation, every mount created through php.mount() is reassigned to the new runtime and every mount created directly via php.FS.mount() is lost.

So this fix replaces the php.FS.mount() call with a php.mount() call. That one is async, so the codebase now has more await statements.

Other changes

Also improved error reporting in sandboxedSpawnHandlerFactory — non-Error exception objects (like Emscripten abort objects) are now logged to stderr instead of being silently swallowed.

Testing instructions

  • CI
  • Also, call proc_open() multiple times and confirm each call produces an output

Copilot AI review requested due to automatic review settings March 5, 2026 17:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@adamziel adamziel force-pushed the fix/proc-open-multiple-calls-v2 branch 8 times, most recently from 80f7fac to 7fd4941 Compare March 5, 2026 20:20
@adamziel adamziel marked this pull request as draft March 5, 2026 22:47
@adamziel adamziel marked this pull request as ready for review March 6, 2026 11:32
adamziel and others added 5 commits March 6, 2026 12:47
When PHP code calls proc_open() with the php binary multiple times in a
row, only the first call returns output. Subsequent calls fail silently.
This breaks any PHP code that spawns multiple PHP subprocesses,
including WP-CLI and Blueprints v2.

The root cause is that main() calls exit(), which in Emscripten throws
an exception that unwinds the stack before cleanup code can run. On
JSPI this leaves stdout/stderr redirected, making the module unusable
for another cli() call. The fix forces runtime rotation before repeated
cli() calls, giving each invocation a fresh WASM module.

Also fixed:
- MEMFS snapshot taken before exit() so WASM memory detach can't
  corrupt the filesystem copy
- CWD restoration after rotation falls back to / instead of throwing
- request.error rotation guard scoped to HTTP-serving instances
- .finally() in cli() only schedules rotation for HTTP-serving instances

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The maxRequests rotation counter was gated on #phpWasmInitCalled, but
that flag isn't set until php_wasm_init() runs inside the execution
function. This meant the very first run() call never incremented the
counter, breaking rotation tests that expected rotation after exactly
maxRequests calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On JSPI, main()→exit() throws an Emscripten exception that prevents
run_cli() from restoring stdout/stderr. This means a PHP instance that
has already called cli() once cannot call it again — the second call
hangs because the module is in a dirty state.

This is the root cause of proc_open(php) failing on the 2nd and 3rd
calls: each proc_open acquires a subprocess instance from the pool,
but the pool reuses instances. The second proc_open gets the same
instance back, tries to call cli() again, and hangs.

The fix: when #cliDirtiedRuntime is true, mark the runtime as needing
rotation before the next cli() call. The #executeWithErrorHandling
method then creates a fresh WASM module via rotateRuntime(), giving
us a clean slate for the next main() call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the spawn handler catches an object that isn't an Error instance
(e.g. an Emscripten ExitStatus or FS.ErrnoError), use JSON.stringify
with Object.getOwnPropertyNames to capture all properties instead of
just showing [object Object].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Subprocess instances (proc_open) need runtime rotation on JSPI because
main()→exit() throws before cleanup code runs, leaving the module in a
dirty state. But rotation was destroying PROXYFS mounts (/wordpress,
/tmp, etc.) because they were set up via direct FS.mount() calls that
bypassed php.mount() and weren't tracked in #mounts.

Changed proxyFileSystem() to register mounts through php.mount() so
hotSwapPHPRuntime() can re-apply them after creating a fresh WASM
module. This also re-patches PROXYFS with mmap support on each
re-mount, ensuring ICU data files remain accessible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adamziel adamziel force-pushed the fix/proc-open-multiple-calls-v2 branch 2 times, most recently from 3bf276d to 3f439a1 Compare March 6, 2026 23:27
@adamziel adamziel marked this pull request as draft March 6, 2026 23:29
Now that PROXYFS mounts are registered via php.mount() and survive
rotation, we can simplify the rotation logic:

- Remove the phpWasmInitCalled guards that prevented rotation for
  CLI-only instances. They were a workaround for when PROXYFS mounts
  weren't tracked in #mounts.

- Remove the cliDirtiedRuntime flag. It was redundant with needsRotating
  – every write was immediately followed by setting needsRotating, and
  every read just checked whether to set needsRotating.

- Remove dead code after main() in run_cli(). PHP CLI's main() always
  calls exit(), which Emscripten turns into a thrown ExitStatus exception.
  The stream restoration, env cleanup, and free() never executed. This
applies identically on both asyncify and JSPI – the comments claiming
  it was JSPI-specific were inaccurate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adamziel adamziel force-pushed the fix/proc-open-multiple-calls-v2 branch from 3f439a1 to 2c9c09e Compare March 7, 2026 08:34
@adamziel adamziel changed the title Fix proc_open(php) failing on repeated calls [PHP] Fix proc_open(php) failing on repeated calls Mar 7, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adamziel adamziel changed the title [PHP] Fix proc_open(php) failing on repeated calls [PHP] Call php.mount instead of php.FS.mount in proxyFileSystem Mar 9, 2026
@adamziel adamziel marked this pull request as ready for review March 9, 2026 22:18
@adamziel adamziel merged commit 154d409 into trunk Mar 9, 2026
42 checks passed
@adamziel adamziel deleted the fix/proc-open-multiple-calls-v2 branch March 9, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants