Skip to content

WritableResourceStream::handleWrite() enters infinite loop on broken TLS socket (EPIPE without PHP warning) #189

Description

@szado

On PHP 8.x with react/socket's SecureServer (TLS), when a remote client disconnects abruptly, epoll reports EPOLLOUT|EPOLLHUP on the dead socket FD. WritableResourceStream::handleWrite() calls fwrite(), which returns false because the kernel write() returns -1 EPIPE. However, PHP's OpenSSL stream wrapper does not call php_error_docref() for SSL_ERROR_SYSCALL+EPIPE errors in all code paths. The set_error_handler capture therefore gets nothing ($error === null).

The guard evaluates to false, so close() is never called. WritableResourceStream then silently passes false to substr() (implicit cast to 0 in non-strict mode), leaving the write buffer unchanged and the write listener active. epoll keeps returning EPOLLOUT|EPOLLHUP, fwrite() keeps returning false silently - 100% CPU lockup.

Reproduction: use react/socket SecureServer with TLS, kill a client with kill -9 while the server has queued writes to that client.

Fix: separate the $sent === false case (hard error - always close) from $sent === 0 (may be transient EAGAIN/WANT_WRITE - only close when PHP warning is present):

// Before
if (($sent === 0 || $sent === false) && $error !== null) {

// After
if ($sent === false || ($sent === 0 && $error !== null)) {

This issue was investigated with the help of AI, but the 100% CPU usage issue is real and after applying the above fix on production, it seems to have disappeared.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions