From 8965b4821d00162c20b3b0bbba798cd83bf72816 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:21:21 +0000 Subject: [PATCH 1/6] Report only the exception when an accept, a connect or a name lookup fails for it A provider's run() that throws, or a scheduler cancelling the waiting coroutine, makes stream_socket_accept(), stream_socket_client(), fsockopen(), stream_socket_server() and stream_socket_sendto() fail with the exception pending. They also warned (Accept failed, Unable to connect, php_network_getaddresses, Failed to parse), and on a listener in the stream exception mode the accept's StreamException replaced the exception, keeping it only as its previous. A connect to a name with several addresses tried the next one after the exception. Without a pending exception the reports stay, an op completed as Cancelled included. --- .../session_resumption_invalid_data.phpt | 2 -- ext/standard/fsock.c | 3 +- ext/standard/streamsfuncs.c | 16 +++++++--- .../streams/hooks/accept-cancelled-warns.phpt | 19 +++++++++++ .../hooks/accept-throws-exception-mode.phpt | 16 ++++++++++ .../tests/streams/hooks/accept-throws.phpt | 15 +++++++++ .../hooks/client-lookup-fails-warns.phpt | 20 ++++++++++++ .../streams/hooks/client-lookup-throws.phpt | 14 ++++++++ .../hooks/connect-throws-one-attempt.phpt | 32 +++++++++++++++++++ .../tests/streams/hooks/connect-throws.phpt | 16 ++++++++++ .../tests/streams/hooks/fsockopen-throws.phpt | 16 ++++++++++ .../tests/streams/hooks/provider-throws.inc | 11 +++++++ .../streams/hooks/sendto-lookup-throws.phpt | 15 +++++++++ .../streams/hooks/server-lookup-throws.phpt | 14 ++++++++ main/network.c | 15 +++++++-- 15 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 ext/standard/tests/streams/hooks/accept-cancelled-warns.phpt create mode 100644 ext/standard/tests/streams/hooks/accept-throws-exception-mode.phpt create mode 100644 ext/standard/tests/streams/hooks/accept-throws.phpt create mode 100644 ext/standard/tests/streams/hooks/client-lookup-fails-warns.phpt create mode 100644 ext/standard/tests/streams/hooks/client-lookup-throws.phpt create mode 100644 ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt create mode 100644 ext/standard/tests/streams/hooks/connect-throws.phpt create mode 100644 ext/standard/tests/streams/hooks/fsockopen-throws.phpt create mode 100644 ext/standard/tests/streams/hooks/provider-throws.inc create mode 100644 ext/standard/tests/streams/hooks/sendto-lookup-throws.phpt create mode 100644 ext/standard/tests/streams/hooks/server-lookup-throws.phpt diff --git a/ext/openssl/tests/session_resumption_invalid_data.phpt b/ext/openssl/tests/session_resumption_invalid_data.phpt index 93d9329ca0e9..6ae19d520456 100644 --- a/ext/openssl/tests/session_resumption_invalid_data.phpt +++ b/ext/openssl/tests/session_resumption_invalid_data.phpt @@ -61,6 +61,4 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); --EXPECTF-- Warning: stream_socket_client(): Failed to enable crypto in %s on line %d - -Warning: stream_socket_client(): Unable to connect to %s in %s on line %d Type error thrown: session_data must be an Openssl\Session instance diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 42115fed5379..06e793602318 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -117,7 +117,8 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) if (port > 0) { efree(hostname); } - if (stream == NULL) { + /* An exception from the connect's operation reports the failure itself */ + if (stream == NULL && !EG(exception)) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s:" ZEND_LONG_FMT " (%s)", host, port, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index de96b484e518..6518103a8194 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -176,7 +176,8 @@ PHP_FUNCTION(stream_socket_client) php_stream_error_operation_end(context); - if (stream == NULL) { + /* An exception from the connect's operation reports the failure itself */ + if (stream == NULL && !EG(exception)) { /* host might contain binary characters */ zend_string *quoted_host = php_addslashes(host); @@ -248,7 +249,8 @@ PHP_FUNCTION(stream_socket_server) php_stream_error_operation_end(context); - if (stream == NULL) { + /* An exception from the lookup's operation reports the failure itself */ + if (stream == NULL && !EG(exception)) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s (%s)", host, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } @@ -329,7 +331,10 @@ PHP_FUNCTION(stream_socket_accept) if (peername) { zend_string_release(peername); } - php_stream_warn(stream, AcceptFailed, "Accept failed: %s", errstr ? ZSTR_VAL(errstr) : "Unknown error"); + /* An exception from the accept's operation reports the failure itself */ + if (!EG(exception)) { + php_stream_warn(stream, AcceptFailed, "Accept failed: %s", errstr ? ZSTR_VAL(errstr) : "Unknown error"); + } RETVAL_FALSE; } @@ -393,7 +398,10 @@ PHP_FUNCTION(stream_socket_sendto) /* parse the address */ if (FAILURE == php_network_parse_network_address_with_port(target_addr, target_addr_len, (struct sockaddr*)&sa, &sl)) { php_stream_error_operation_end_for_stream(stream); - php_error_docref(NULL, E_WARNING, "Failed to parse `%s' into a valid network address", target_addr); + /* An exception from the lookup's operation reports the failure itself */ + if (!EG(exception)) { + php_error_docref(NULL, E_WARNING, "Failed to parse `%s' into a valid network address", target_addr); + } RETURN_FALSE; } } diff --git a/ext/standard/tests/streams/hooks/accept-cancelled-warns.phpt b/ext/standard/tests/streams/hooks/accept-cancelled-warns.phpt new file mode 100644 index 000000000000..0411bf35989b --- /dev/null +++ b/ext/standard/tests/streams/hooks/accept-cancelled-warns.phpt @@ -0,0 +1,19 @@ +--TEST-- +IO hooks: stream_socket_accept() whose operation completes Cancelled with no exception still warns +--FILE-- +complete(Io\CompletionStatus::Cancelled); + } +}); + +var_dump(stream_socket_accept($server, 5)); +?> +--EXPECTF-- +Warning: stream_socket_accept(): Accept failed: %s in %s on line %d +bool(false) diff --git a/ext/standard/tests/streams/hooks/accept-throws-exception-mode.phpt b/ext/standard/tests/streams/hooks/accept-throws-exception-mode.phpt new file mode 100644 index 000000000000..74bf9e0c5545 --- /dev/null +++ b/ext/standard/tests/streams/hooks/accept-throws-exception-mode.phpt @@ -0,0 +1,16 @@ +--TEST-- +IO hooks: stream_socket_accept() whose operation throws, on a listener in the exception mode, throws that exception +--FILE-- + ['error_mode' => StreamErrorMode::Exception]]); +$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); +require __DIR__ . '/provider-throws.inc'; + +try { + var_dump(stream_socket_accept($server, 5)); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +RuntimeException: cancelled Io\Operation\Accept diff --git a/ext/standard/tests/streams/hooks/accept-throws.phpt b/ext/standard/tests/streams/hooks/accept-throws.phpt new file mode 100644 index 000000000000..cf7510db4831 --- /dev/null +++ b/ext/standard/tests/streams/hooks/accept-throws.phpt @@ -0,0 +1,15 @@ +--TEST-- +IO hooks: stream_socket_accept() whose operation throws reports only the exception +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\Accept diff --git a/ext/standard/tests/streams/hooks/client-lookup-fails-warns.phpt b/ext/standard/tests/streams/hooks/client-lookup-fails-warns.phpt new file mode 100644 index 000000000000..7e79ed17fd08 --- /dev/null +++ b/ext/standard/tests/streams/hooks/client-lookup-fails-warns.phpt @@ -0,0 +1,20 @@ +--TEST-- +IO hooks: stream_socket_client() whose name lookup finds no address with no exception still warns +--FILE-- +completeWithAddresses([]); + } +}); + +var_dump(stream_socket_client('tcp://nowhere.provider:80', $errno, $errstr, 5)); +?> +--EXPECTF-- +Warning: stream_socket_client(): php_network_getaddresses: getaddrinfo for nowhere.provider failed: %s in %s on line %d + +Warning: stream_socket_client(): Unable to connect to tcp://nowhere.provider:80 (php_network_getaddresses: getaddrinfo for nowhere.provider failed: %s) in %s on line %d +bool(false) diff --git a/ext/standard/tests/streams/hooks/client-lookup-throws.phpt b/ext/standard/tests/streams/hooks/client-lookup-throws.phpt new file mode 100644 index 000000000000..36166f178557 --- /dev/null +++ b/ext/standard/tests/streams/hooks/client-lookup-throws.phpt @@ -0,0 +1,14 @@ +--TEST-- +IO hooks: stream_socket_client() whose name lookup throws reports only the exception +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\GetAddrInfo diff --git a/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt new file mode 100644 index 000000000000..913cd8f62886 --- /dev/null +++ b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt @@ -0,0 +1,32 @@ +--TEST-- +IO hooks: a connect whose operation throws tries no further address +--FILE-- +completeWithAddresses(['127.0.0.1', '127.0.0.2']); + } + $this->connects++; + throw new RuntimeException("cancelled " . $op::class); + } +}; +Io\Hooks\set_hooks($provider); + +try { + var_dump(stream_socket_client("tcp://two.addresses:$port", $errno, $errstr, 5)); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($provider->connects); +?> +--EXPECT-- +cancelled Io\Operation\Connect +int(1) diff --git a/ext/standard/tests/streams/hooks/connect-throws.phpt b/ext/standard/tests/streams/hooks/connect-throws.phpt new file mode 100644 index 000000000000..1f1d8e688f76 --- /dev/null +++ b/ext/standard/tests/streams/hooks/connect-throws.phpt @@ -0,0 +1,16 @@ +--TEST-- +IO hooks: stream_socket_client() whose connect throws reports only the exception +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\Connect diff --git a/ext/standard/tests/streams/hooks/fsockopen-throws.phpt b/ext/standard/tests/streams/hooks/fsockopen-throws.phpt new file mode 100644 index 000000000000..cbba96289e24 --- /dev/null +++ b/ext/standard/tests/streams/hooks/fsockopen-throws.phpt @@ -0,0 +1,16 @@ +--TEST-- +IO hooks: fsockopen() whose connect throws reports only the exception +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\Connect diff --git a/ext/standard/tests/streams/hooks/provider-throws.inc b/ext/standard/tests/streams/hooks/provider-throws.inc new file mode 100644 index 000000000000..a5f116050057 --- /dev/null +++ b/ext/standard/tests/streams/hooks/provider-throws.inc @@ -0,0 +1,11 @@ +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\GetAddrInfo diff --git a/ext/standard/tests/streams/hooks/server-lookup-throws.phpt b/ext/standard/tests/streams/hooks/server-lookup-throws.phpt new file mode 100644 index 000000000000..341d7e6aab3f --- /dev/null +++ b/ext/standard/tests/streams/hooks/server-lookup-throws.phpt @@ -0,0 +1,14 @@ +--TEST-- +IO hooks: stream_socket_server() whose name lookup throws reports only the exception +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\GetAddrInfo diff --git a/main/network.c b/main/network.c index 332d9a352e3a..ae0edc9c38ef 100644 --- a/main/network.c +++ b/main/network.c @@ -200,8 +200,9 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka zend_string_release_ex(*error_string, 0); } *error_string = strpprintf(0, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); - php_error_docref(NULL, E_WARNING, "%s", ZSTR_VAL(*error_string)); - } else { + } + /* An exception from the lookup's operation reports the failure itself */ + if (!EG(exception)) { php_error_docref(NULL, E_WARNING, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); } # ifdef PHP_WIN32 @@ -641,7 +642,10 @@ PHPAPI zend_result php_network_parse_network_address_with_port(const char *addr, if (n == 0) { if (errstr) { - php_error_docref(NULL, E_WARNING, "Failed to resolve `%s': %s", tmp, ZSTR_VAL(errstr)); + /* An exception from the lookup's operation reports the failure itself */ + if (!EG(exception)) { + php_error_docref(NULL, E_WARNING, "Failed to resolve `%s': %s", tmp, ZSTR_VAL(errstr)); + } zend_string_release_ex(errstr, 0); } goto out; @@ -1090,6 +1094,11 @@ php_socket_t php_network_connect_socket_to_host_stream(php_stream *stream, php_s *current = SOCK_ERR; } + /* An exception from the connect's operation ends the attempts */ + if (EG(exception)) { + fatal = 1; + } + /* adjust timeout for next attempt */ #ifdef HAVE_GETTIMEOFDAY if (timeout) { From 3beb801a6ab88913f84fff77bb8f48aefb6556dc Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:29:18 +0000 Subject: [PATCH 2/6] End a TLS handshake whose wait is cancelled instead of spinning to the deadline The stream BIO turns a cancelled Recv or Send into a retry and sets io_cancelled, which the read and write loops check; the handshake loop did not. A handshake whose wait threw, as a scheduler's cancellation does, retried at once, the next wait was refused for the pending exception, and the loop spun at full CPU until the connect timeout (without one, for good), then warned "SSL: Handshake timed out" and "Failed to enable crypto" on top of the exception. It now ends at the first cancelled wait, and the crypto failure is reported only without a pending exception. --- .../io_hooks_handshake_cancelled_accept.phpt | 41 +++++++++++++++++++ .../io_hooks_handshake_cancelled_client.phpt | 31 ++++++++++++++ .../session_resumption_invalid_data.phpt | 2 - ext/openssl/xp_ssl.c | 16 +++++++- 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 ext/openssl/tests/io_hooks_handshake_cancelled_accept.phpt create mode 100644 ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt diff --git a/ext/openssl/tests/io_hooks_handshake_cancelled_accept.phpt b/ext/openssl/tests/io_hooks_handshake_cancelled_accept.phpt new file mode 100644 index 000000000000..e1193ed1ef98 --- /dev/null +++ b/ext/openssl/tests/io_hooks_handshake_cancelled_accept.phpt @@ -0,0 +1,41 @@ +--TEST-- +IO hooks: a TLS server handshake whose wait throws ends at once and reports only the exception +--EXTENSIONS-- +openssl +--FILE-- +saveNewCertAsFileWithKey('io_hooks_handshake_cancelled_accept', $certFile); + +$context = stream_context_create(['ssl' => ['local_cert' => $certFile]]); +$server = stream_socket_server('ssl://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); + +/* The client connects and never sends a ClientHello */ +$client = stream_socket_client('tcp://' . stream_socket_get_name($server, false)); + +Io\Hooks\set_hooks(new class implements Io\Hooks\Hooks { + public function getCapabilities(): array { return []; } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + return match (true) { + $op instanceof Io\Operation\Accept, $op instanceof Io\Operation\Send + => $op->complete(Io\CompletionStatus::Unsupported), + default => throw new RuntimeException("cancelled " . $op::class), + }; + } +}); + +try { + var_dump(stream_socket_accept($server, 2)); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +cancelled Io\Operation\Recv diff --git a/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt b/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt new file mode 100644 index 000000000000..137724ff572e --- /dev/null +++ b/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt @@ -0,0 +1,31 @@ +--TEST-- +IO hooks: a TLS client handshake whose wait throws ends at once and reports only the exception +--EXTENSIONS-- +openssl +--FILE-- + $op->completeReady($op->getEvents()), + $op instanceof Io\Operation\Send => $op->complete(Io\CompletionStatus::Unsupported), + default => throw new RuntimeException("cancelled " . $op::class), + }; + } +}); + +try { + var_dump(stream_socket_client("tls://$address", $errno, $errstr, 2)); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +cancelled Io\Operation\Recv diff --git a/ext/openssl/tests/session_resumption_invalid_data.phpt b/ext/openssl/tests/session_resumption_invalid_data.phpt index 6ae19d520456..b044a3b4d8f4 100644 --- a/ext/openssl/tests/session_resumption_invalid_data.phpt +++ b/ext/openssl/tests/session_resumption_invalid_data.phpt @@ -59,6 +59,4 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_invalid.pem.tmp'); ?> --EXPECTF-- - -Warning: stream_socket_client(): Failed to enable crypto in %s on line %d Type error thrown: session_data must be an Openssl\Session instance diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index f0761355d2de..9b952d9f9093 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3188,6 +3188,7 @@ static int php_openssl_enable_crypto(php_stream *stream, sslsock->is_client ? &sslsock->connect_timeout : &sslsock->s.timeout)); } sslsock->deadline = blocked ? &deadline : NULL; + sslsock->io_cancelled = 0; do { ERR_clear_error(); @@ -3196,6 +3197,11 @@ static int php_openssl_enable_crypto(php_stream *stream, break; } retry = php_openssl_handle_ssl_error(stream, n, blocked); + if (sslsock->io_cancelled) { + /* The wait was cancelled behind a retry, which would spin to the deadline */ + sslsock->deadline = NULL; + return -1; + } if (retry && php_deadline_to_timeout_ms(&deadline) == 0) { sslsock->deadline = NULL; php_stream_warn(stream, TimeOut, "SSL: Handshake timed out"); @@ -3551,7 +3557,10 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ if (php_stream_xport_crypto_setup(xparam->outputs.client, clisockdata->method, sock->ctx ? stream : NULL) < 0 || php_stream_xport_crypto_enable( xparam->outputs.client, 1) < 0) { - php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + /* An exception from the handshake's operation reports the failure itself */ + if (!EG(exception)) { + php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + } php_stream_close(xparam->outputs.client); xparam->outputs.client = NULL; @@ -3765,7 +3774,10 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 || php_stream_xport_crypto_enable(stream, 1) < 0) { - php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + /* An exception from the handshake's operation reports the failure itself */ + if (!EG(exception)) { + php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + } xparam->outputs.returncode = -1; } } From 1da1a2c173fffbf928e58edb74341a00257af7c9 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:41:09 +0000 Subject: [PATCH 3/6] Let the connect-throws tests accept the Poll that waits for a started connect With the connect-started fix (bukka/php-src#4) the core waits for a connect it started itself with a Poll op, not a Connect op, so the throwing provider names a Poll. The tests check that only the exception is reported, whichever op the provider was given. The one-attempt test counted the provider's ops, but after the exception the core runs no op through the provider, so the count stayed 1 even when the next address was tried. It now resolves the name to the listener twice and expects at most one connection there. --- .../hooks/connect-throws-one-attempt.phpt | 24 +++++++++++-------- .../tests/streams/hooks/connect-throws.phpt | 4 ++-- .../tests/streams/hooks/fsockopen-throws.phpt | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt index 913cd8f62886..34aeb88e0440 100644 --- a/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt +++ b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt @@ -5,28 +5,32 @@ IO hooks: a connect whose operation throws tries no further address $server = stream_socket_server('tcp://127.0.0.1:0'); $port = (int) substr(strrchr(stream_socket_get_name($server, false), ':'), 1); -$provider = new class implements Io\Hooks\Hooks { - public int $connects = 0; +Io\Hooks\set_hooks(new class implements Io\Hooks\Hooks { public function getCapabilities(): array { return []; } public function add(Io\Registration $registration): void {} public function remove(Io\Registration $registration): void {} public function run(Io\Operation $op): Io\Completion { if ($op instanceof Io\Operation\GetAddrInfo) { - return $op->completeWithAddresses(['127.0.0.1', '127.0.0.2']); + return $op->completeWithAddresses(['127.0.0.1', '127.0.0.1']); } - $this->connects++; throw new RuntimeException("cancelled " . $op::class); } -}; -Io\Hooks\set_hooks($provider); +}); try { var_dump(stream_socket_client("tcp://two.addresses:$port", $errno, $errstr, 5)); } catch (RuntimeException $e) { echo $e->getMessage(), "\n"; } -var_dump($provider->connects); +Io\Hooks\set_hooks(null); +/* Each attempt's own connect() reaches the listener whatever the provider does; the first one's + * handshake may not complete before the socket is closed */ +$accepted = 0; +while (@stream_socket_accept($server, 0.2)) { + $accepted++; +} +var_dump($accepted <= 1); ?> ---EXPECT-- -cancelled Io\Operation\Connect -int(1) +--EXPECTREGEX-- +cancelled Io\\Operation\\(Connect|Poll) +bool\(true\) diff --git a/ext/standard/tests/streams/hooks/connect-throws.phpt b/ext/standard/tests/streams/hooks/connect-throws.phpt index 1f1d8e688f76..d952d6c37aed 100644 --- a/ext/standard/tests/streams/hooks/connect-throws.phpt +++ b/ext/standard/tests/streams/hooks/connect-throws.phpt @@ -12,5 +12,5 @@ try { echo $e->getMessage(), "\n"; } ?> ---EXPECT-- -cancelled Io\Operation\Connect +--EXPECTREGEX-- +cancelled Io\\Operation\\(Connect|Poll) diff --git a/ext/standard/tests/streams/hooks/fsockopen-throws.phpt b/ext/standard/tests/streams/hooks/fsockopen-throws.phpt index cbba96289e24..9080d9190058 100644 --- a/ext/standard/tests/streams/hooks/fsockopen-throws.phpt +++ b/ext/standard/tests/streams/hooks/fsockopen-throws.phpt @@ -12,5 +12,5 @@ try { echo $e->getMessage(), "\n"; } ?> ---EXPECT-- -cancelled Io\Operation\Connect +--EXPECTREGEX-- +cancelled Io\\Operation\\(Connect|Poll) From ac2680b4a64241fafab1318ec4c2b3b91aecd6b9 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:06:59 +0000 Subject: [PATCH 4/6] Drop the repeated comment on the warnings skipped for a pending exception The !EG(exception) guard before a warning is the usual idiom; the commits that added it say why. --- ext/openssl/xp_ssl.c | 2 -- ext/standard/fsock.c | 1 - ext/standard/streamsfuncs.c | 4 ---- main/network.c | 2 -- 4 files changed, 9 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 9b952d9f9093..8786a89baf29 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3557,7 +3557,6 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ if (php_stream_xport_crypto_setup(xparam->outputs.client, clisockdata->method, sock->ctx ? stream : NULL) < 0 || php_stream_xport_crypto_enable( xparam->outputs.client, 1) < 0) { - /* An exception from the handshake's operation reports the failure itself */ if (!EG(exception)) { php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); } @@ -3774,7 +3773,6 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 || php_stream_xport_crypto_enable(stream, 1) < 0) { - /* An exception from the handshake's operation reports the failure itself */ if (!EG(exception)) { php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); } diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 06e793602318..4903979276e0 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -117,7 +117,6 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) if (port > 0) { efree(hostname); } - /* An exception from the connect's operation reports the failure itself */ if (stream == NULL && !EG(exception)) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s:" ZEND_LONG_FMT " (%s)", host, port, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 6518103a8194..4b5a971d1ba0 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -176,7 +176,6 @@ PHP_FUNCTION(stream_socket_client) php_stream_error_operation_end(context); - /* An exception from the connect's operation reports the failure itself */ if (stream == NULL && !EG(exception)) { /* host might contain binary characters */ zend_string *quoted_host = php_addslashes(host); @@ -249,7 +248,6 @@ PHP_FUNCTION(stream_socket_server) php_stream_error_operation_end(context); - /* An exception from the lookup's operation reports the failure itself */ if (stream == NULL && !EG(exception)) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s (%s)", host, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } @@ -331,7 +329,6 @@ PHP_FUNCTION(stream_socket_accept) if (peername) { zend_string_release(peername); } - /* An exception from the accept's operation reports the failure itself */ if (!EG(exception)) { php_stream_warn(stream, AcceptFailed, "Accept failed: %s", errstr ? ZSTR_VAL(errstr) : "Unknown error"); } @@ -398,7 +395,6 @@ PHP_FUNCTION(stream_socket_sendto) /* parse the address */ if (FAILURE == php_network_parse_network_address_with_port(target_addr, target_addr_len, (struct sockaddr*)&sa, &sl)) { php_stream_error_operation_end_for_stream(stream); - /* An exception from the lookup's operation reports the failure itself */ if (!EG(exception)) { php_error_docref(NULL, E_WARNING, "Failed to parse `%s' into a valid network address", target_addr); } diff --git a/main/network.c b/main/network.c index ae0edc9c38ef..d3db4ccc1fb0 100644 --- a/main/network.c +++ b/main/network.c @@ -201,7 +201,6 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka } *error_string = strpprintf(0, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); } - /* An exception from the lookup's operation reports the failure itself */ if (!EG(exception)) { php_error_docref(NULL, E_WARNING, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); } @@ -642,7 +641,6 @@ PHPAPI zend_result php_network_parse_network_address_with_port(const char *addr, if (n == 0) { if (errstr) { - /* An exception from the lookup's operation reports the failure itself */ if (!EG(exception)) { php_error_docref(NULL, E_WARNING, "Failed to resolve `%s': %s", tmp, ZSTR_VAL(errstr)); } From d735cab2136edefe779eecd2482d92bc380d53f5 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:10:23 +0000 Subject: [PATCH 5/6] Mark the exception checks EXPECTED/UNEXPECTED and make their comments plain --- ext/openssl/xp_ssl.c | 8 ++++---- ext/standard/fsock.c | 2 +- ext/standard/streamsfuncs.c | 8 ++++---- .../tests/streams/hooks/connect-throws-one-attempt.phpt | 4 ++-- main/network.c | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 8786a89baf29..084a54d64991 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3197,8 +3197,8 @@ static int php_openssl_enable_crypto(php_stream *stream, break; } retry = php_openssl_handle_ssl_error(stream, n, blocked); - if (sslsock->io_cancelled) { - /* The wait was cancelled behind a retry, which would spin to the deadline */ + if (UNEXPECTED(sslsock->io_cancelled)) { + /* A cancelled wait comes back as a retry; retrying would spin until the deadline */ sslsock->deadline = NULL; return -1; } @@ -3557,7 +3557,7 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ if (php_stream_xport_crypto_setup(xparam->outputs.client, clisockdata->method, sock->ctx ? stream : NULL) < 0 || php_stream_xport_crypto_enable( xparam->outputs.client, 1) < 0) { - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); } @@ -3773,7 +3773,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 || php_stream_xport_crypto_enable(stream, 1) < 0) { - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); } xparam->outputs.returncode = -1; diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 4903979276e0..2d568b7d3e3e 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -117,7 +117,7 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) if (port > 0) { efree(hostname); } - if (stream == NULL && !EG(exception)) { + if (stream == NULL && EXPECTED(!EG(exception))) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s:" ZEND_LONG_FMT " (%s)", host, port, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 4b5a971d1ba0..1a8896aa157d 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -176,7 +176,7 @@ PHP_FUNCTION(stream_socket_client) php_stream_error_operation_end(context); - if (stream == NULL && !EG(exception)) { + if (stream == NULL && EXPECTED(!EG(exception))) { /* host might contain binary characters */ zend_string *quoted_host = php_addslashes(host); @@ -248,7 +248,7 @@ PHP_FUNCTION(stream_socket_server) php_stream_error_operation_end(context); - if (stream == NULL && !EG(exception)) { + if (stream == NULL && EXPECTED(!EG(exception))) { php_error_docref(NULL, E_WARNING, "Unable to connect to %s (%s)", host, errstr == NULL ? "Unknown error" : ZSTR_VAL(errstr)); } @@ -329,7 +329,7 @@ PHP_FUNCTION(stream_socket_accept) if (peername) { zend_string_release(peername); } - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_stream_warn(stream, AcceptFailed, "Accept failed: %s", errstr ? ZSTR_VAL(errstr) : "Unknown error"); } RETVAL_FALSE; @@ -395,7 +395,7 @@ PHP_FUNCTION(stream_socket_sendto) /* parse the address */ if (FAILURE == php_network_parse_network_address_with_port(target_addr, target_addr_len, (struct sockaddr*)&sa, &sl)) { php_stream_error_operation_end_for_stream(stream); - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_error_docref(NULL, E_WARNING, "Failed to parse `%s' into a valid network address", target_addr); } RETURN_FALSE; diff --git a/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt index 34aeb88e0440..47b91a290883 100644 --- a/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt +++ b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt @@ -23,8 +23,8 @@ try { echo $e->getMessage(), "\n"; } Io\Hooks\set_hooks(null); -/* Each attempt's own connect() reaches the listener whatever the provider does; the first one's - * handshake may not complete before the socket is closed */ +/* Each attempt's connect() reaches the listener even when the provider throws, so a second attempt + * would show as a second connection; the first may close before it is accepted */ $accepted = 0; while (@stream_socket_accept($server, 0.2)) { $accepted++; diff --git a/main/network.c b/main/network.c index d3db4ccc1fb0..dd40a65b1a7e 100644 --- a/main/network.c +++ b/main/network.c @@ -201,7 +201,7 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka } *error_string = strpprintf(0, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); } - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_error_docref(NULL, E_WARNING, "php_network_getaddresses: getaddrinfo for %s failed: %s", host, gai_error); } # ifdef PHP_WIN32 @@ -641,7 +641,7 @@ PHPAPI zend_result php_network_parse_network_address_with_port(const char *addr, if (n == 0) { if (errstr) { - if (!EG(exception)) { + if (EXPECTED(!EG(exception))) { php_error_docref(NULL, E_WARNING, "Failed to resolve `%s': %s", tmp, ZSTR_VAL(errstr)); } zend_string_release_ex(errstr, 0); @@ -1092,8 +1092,8 @@ php_socket_t php_network_connect_socket_to_host_stream(php_stream *stream, php_s *current = SOCK_ERR; } - /* An exception from the connect's operation ends the attempts */ - if (EG(exception)) { + /* No next address after an exception, e.g. a cancellation */ + if (UNEXPECTED(EG(exception))) { fatal = 1; } From e0e3cce244417697de602b6cced6f274beac7967 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:17:43 +0000 Subject: [PATCH 6/6] Let the TLS client handshake test complete the Poll that waits for a started connect With the connect-started fix (bukka/php-src#4) the core waits for its own connect with a Poll op, which the provider threw on, so the test stopped before the handshake and named a Poll. --- ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt b/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt index 137724ff572e..57e0a0e41bd2 100644 --- a/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt +++ b/ext/openssl/tests/io_hooks_handshake_cancelled_client.phpt @@ -14,7 +14,7 @@ Io\Hooks\set_hooks(new class implements Io\Hooks\Hooks { public function remove(Io\Registration $registration): void {} public function run(Io\Operation $op): Io\Completion { return match (true) { - $op instanceof Io\Operation\Connect => $op->completeReady($op->getEvents()), + $op instanceof Io\Operation\Connect, $op instanceof Io\Operation\Poll => $op->completeReady($op->getEvents()), $op instanceof Io\Operation\Send => $op->complete(Io\CompletionStatus::Unsupported), default => throw new RuntimeException("cancelled " . $op::class), };