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.
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):
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.