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..57e0a0e41bd2 --- /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 93d9329ca0e9..b044a3b4d8f4 100644 --- a/ext/openssl/tests/session_resumption_invalid_data.phpt +++ b/ext/openssl/tests/session_resumption_invalid_data.phpt @@ -59,8 +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 - -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/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index f0761355d2de..084a54d64991 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 (UNEXPECTED(sslsock->io_cancelled)) { + /* A cancelled wait comes back as a retry; retrying would spin until 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,9 @@ 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"); + if (EXPECTED(!EG(exception))) { + php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + } php_stream_close(xparam->outputs.client); xparam->outputs.client = NULL; @@ -3765,7 +3773,9 @@ 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"); + 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 42115fed5379..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) { + 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 de96b484e518..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) { + 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) { + 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,9 @@ 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"); + if (EXPECTED(!EG(exception))) { + php_stream_warn(stream, AcceptFailed, "Accept failed: %s", errstr ? ZSTR_VAL(errstr) : "Unknown error"); + } RETVAL_FALSE; } @@ -393,7 +395,9 @@ 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); + 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/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..47b91a290883 --- /dev/null +++ b/ext/standard/tests/streams/hooks/connect-throws-one-attempt.phpt @@ -0,0 +1,36 @@ +--TEST-- +IO hooks: a connect whose operation throws tries no further address +--FILE-- +completeWithAddresses(['127.0.0.1', '127.0.0.1']); + } + throw new RuntimeException("cancelled " . $op::class); + } +}); + +try { + var_dump(stream_socket_client("tcp://two.addresses:$port", $errno, $errstr, 5)); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +/* 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++; +} +var_dump($accepted <= 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 new file mode 100644 index 000000000000..d952d6c37aed --- /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"; +} +?> +--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 new file mode 100644 index 000000000000..9080d9190058 --- /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"; +} +?> +--EXPECTREGEX-- +cancelled Io\\Operation\\(Connect|Poll) 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..dd40a65b1a7e 100644 --- a/main/network.c +++ b/main/network.c @@ -200,8 +200,8 @@ 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 { + } + 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,9 @@ 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)); + 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); } goto out; @@ -1090,6 +1092,11 @@ php_socket_t php_network_connect_socket_to_host_stream(php_stream *stream, php_s *current = SOCK_ERR; } + /* No next address after an exception, e.g. a cancellation */ + if (UNEXPECTED(EG(exception))) { + fatal = 1; + } + /* adjust timeout for next attempt */ #ifdef HAVE_GETTIMEOFDAY if (timeout) {