From 7150b38ab3a98cf98a1a98aa7d0792935ae5e1bc Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 30 May 2026 10:52:35 +0200 Subject: [PATCH] Fix GH-22081: Memory leak in php_openssl_enable_crypto This was leaking because php_openssl_enable_crypto can be called multiple times. The reneg was moved there after the session changes so it needs to only happen once there. The fix moves it (and some other parts that should be done just once) inside state_set block where it can run only once. --- ext/openssl/tests/gh22081.phpt | 76 ++++++++++++++++++++++++++++++++++ ext/openssl/xp_ssl.c | 29 ++++++------- 2 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 ext/openssl/tests/gh22081.phpt diff --git a/ext/openssl/tests/gh22081.phpt b/ext/openssl/tests/gh22081.phpt new file mode 100644 index 000000000000..17f74be7584c --- /dev/null +++ b/ext/openssl/tests/gh22081.phpt @@ -0,0 +1,76 @@ +--TEST-- +GH-22081: server reneg limit not reallocated across non-blocking retries +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + /* Plain TCP listener so the TLS handshake is driven manually below. */ + $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Complete each handshake in non-blocking mode so that php_openssl_enable_crypto() is + * re-entered across multiple WANT_READ/WANT_WRITE rounds per connection. */ + for ($i = 0; $i < 5; $i++) { + $conn = @stream_socket_accept($server, 30); + if (!$conn) { + continue; + } + stream_set_blocking($conn, false); + do { + $r = stream_socket_enable_crypto($conn, true, STREAM_CRYPTO_METHOD_TLS_SERVER); + } while ($r === 0); + + if ($r === true) { + fwrite($conn, "ok $i\n"); + } + fclose($conn); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]]); + + for ($i = 0; $i < 5; $i++) { + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client) { + echo trim(fgets($client)) . "\n"; + fclose($client); + } + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('gh22081-server-reneg-nonblocking', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +ok 0 +ok 1 +ok 2 +ok 3 +ok 4 + diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 4d0dad20a439..307cc3489c3c 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2688,28 +2688,23 @@ static int php_openssl_enable_crypto(php_stream *stream, struct timeval start_time, *timeout; bool blocked = sslsock->s.is_blocked, has_timeout = false; - if (sslsock->is_client) { - /* Set session data for client */ - if ( php_openssl_apply_client_session_data(stream, sslsock)) { - return FAILURE; - } -#ifdef HAVE_TLS_SNI - php_openssl_enable_client_sni(stream, sslsock); -#endif - } else { - php_openssl_init_server_reneg_limit(stream, sslsock); - } - + if (!sslsock->state_set) { #ifdef PHP_OPENSSL_TLS_DEBUG - BIO *b_out = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT); - SSL_set_msg_callback(sslsock->ssl_handle, SSL_trace); - SSL_set_msg_callback_arg(sslsock->ssl_handle, b_out); + BIO *b_out = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT); + SSL_set_msg_callback(sslsock->ssl_handle, SSL_trace); + SSL_set_msg_callback_arg(sslsock->ssl_handle, b_out); #endif - - if (!sslsock->state_set) { if (sslsock->is_client) { + /* Set session data for client */ + if (php_openssl_apply_client_session_data(stream, sslsock) == FAILURE) { + return -1; + } +#ifdef HAVE_TLS_SNI + php_openssl_enable_client_sni(stream, sslsock); +#endif SSL_set_connect_state(sslsock->ssl_handle); } else { + php_openssl_init_server_reneg_limit(stream, sslsock); SSL_set_accept_state(sslsock->ssl_handle); } sslsock->state_set = 1;