Skip to content

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

@Runnin4ik

Description

@Runnin4ik

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions