amphp / amphp/process

Windows: Process::start hangs for exactly 60s with ext-uv (Handle Inheritance Deadlock in ProcessWrapper)

Open
#80 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
260
Forks
29
PR merge metrics
No merged PRs in 30d

Description

I have identified a critical blocking issue when using amphp/process on Windows with the uv extension enabled (using Revolt\EventLoop\Driver\UvDriver).

Any attempt to spawn a process results in a 60-second hang, rendering the library unusable. This also breaks amphp/dns out-of-the-box on Windows, as its default WindowsDnsConfigLoader spawns a PowerShell process to retrieve system DNS settings.

Environment:
  • OS: Windows 11
  • PHP: 8.5.2
  • Extensions: uv (enabled)
  • Driver: Revolt\EventLoop\Driver\UvDriver
  • Package: amphp/process (latest 2.0.3) & amphp/windows-process-wrapper (v1.2.0)
Reproduction Script:

The hang occurs only when UvDriver is active. Using StreamDriver works instantly.

<?php
require __DIR__ . '/vendor/autoload.php';

use Amp\Process\Process;
use Revolt\EventLoop;

echo "Driver: " . get_class(EventLoop::getDriver()) . "\n";

$start = microtime(true);
echo "Starting process...\n";

// This line hangs for exactly 60 seconds
$process = Process::start('cmd /c echo Hello');

$process->join();
echo "Done in " . (microtime(true) - $start) . "s\n";
Deep Analysis & Debugging:

I performed extensive debugging by modifying both the PHP SocketConnector and recompiling the C ProcessWrapper.exe with logging.

Trace Findings:

  1. TCP Connection: ProcessWrapper.exe successfully connects to the PHP server socket (127.0.0.1:0).
  2. Handshake: The security token exchange completes instantly.
  3. The Deadlock: The data transmission hangs exactly when the Wrapper calls CreateProcessW to spawn the actual command (e.g., cmd.exe).
  4. Wrapper Internal State: My custom logs inside the Wrapper C-code show that it successfully sends the PID signal and calls send(). It finishes its work instantly (< 1s).
  5. PHP State: UvDriver does not receive the onReadable event for the PID data until exactly 60 seconds later (Windows TCP timeout).
Root Cause: IOCP Handle Inheritance Deadlock

The issue is likely caused by Socket Handle Inheritance combined with libuv's IOCP implementation.

  1. The Leak: PHP's proc_open on Windows hardcodes bInheritHandles = TRUE. This causes the ProcessWrapper.exe to inherit the parent's (PHP) TCP server socket handle.
  2. The Deadlock: ext-uv ties all sockets to an IOCP port. When the ProcessWrapper.exe spawns the actual child process, that "grandchild" also inherits the server socket handle.
  3. The Conflict: The server socket handle is now held by a grandchild process while being bound to the parent's IOCP. The Windows kernel detects a conflict or deadlock in the I/O completion mechanism for this socket. The send() call in the Wrapper blocks (or the OS buffers the data but doesn't notify the parent) until the system TCP timeout (60s) forces a reset/flush.
Verification of Potential Fixes

I tried to fix this by modifying windows-process-wrapper (C-code) to use Selective Handle Inheritance:

  • I utilized STARTUPINFOEX with UpdateProcThreadAttribute and PROC_THREAD_ATTRIBUTE_HANDLE_LIST.
  • I explicitly whitelisted only the 3 pipes (STDIN/STDOUT/STDERR) for inheritance.
  • Result: The 60-second hang persisted.

Even though the Wrapper logic ensured the socket handle wasn't passed to the grandchild, the deadlock remained. This suggests that the conflict is deeper within libuv/PHP interaction or that proc_open's initial handle leakage cannot be fully mitigated by the child process wrapper alone.

Conclusion & Solution

Since patching the Wrapper to filter handles did not resolve the issue, and fixing proc_open in PHP core is out of scope, the architecture of amphp/process on Windows appears incompatible with ext-uv when using TCP sockets for IPC.

Proposed Long-Term Fix:
When UvDriver is detected on Windows, the library should ideally switch to using Named Pipes (\\.\pipe\...) instead of TCP sockets. Named Pipes are natively supported by libuv (and IOCP) and handle inheritance/duplication much more gracefully than TCP sockets in this context.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the supplied Process::start reproduction with UvDriver and StreamDriver, then inspect the PHP SocketConnector and windows-process-wrapper C code described in the report. Done means the Windows UvDriver path no longer hangs for 60 seconds when spawning a process; the proposed direction is to evaluate named-pipe IPC.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend, operating-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.