Commit 12631df9c9d for php

commit 12631df9c9d17cb351cc37fd3d2c832ed507b214
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Wed Nov 4 18:29:54 2026 -0500

    ext/openssl: Fix shared SSL_CTX callbacks reading a freed session stream

    Since b1242c32bca a server stream seeded from a session stream shares the
    parent's SSL_CTX. Two callbacks on that context were registered with the
    parent's stream as their argument: the ALPN select callback and, on Windows
    without a CA file, the certificate verify callback. Closing the parent
    before the child completed its handshake left them reading freed memory, and
    the child silently negotiated no ALPN protocol. Look up the handshake's own
    stream from the SSL ex_data, as verify_callback and the SNI callback do, and
    give the child its own copy of the ALPN protocol list.

    Closes GH-25211

diff --git a/NEWS b/NEWS
index f702d1bb3d0..9a901f31dbb 100644
--- a/NEWS
+++ b/NEWS
@@ -127,6 +127,13 @@ PHP                                                                        NEWS
   . Fixed bug GH-15701 (Build failure: incompatible funopen() seeker pointer
     type on 32-bit platforms). (Kamil Tekiela)

+?? ??? ????, PHP 8.6.0
+
+- OpenSSL:
+  . Fixed use-after-free in TLS server ALPN and Windows certificate checks
+    when the session stream passed to stream_socket_enable_crypto() is closed
+    before the handshake. (Ilia Alshanetsky)
+
 12 Nov 2026, PHP 8.6.0RC6

 - Core:
diff --git a/ext/openssl/tests/session_resumption_server_alpn_closed_parent.phpt b/ext/openssl/tests/session_resumption_server_alpn_closed_parent.phpt
new file mode 100644
index 00000000000..f612fd5fbf9
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_server_alpn_closed_parent.phpt
@@ -0,0 +1,59 @@
+--TEST--
+Server ALPN selection on a stream seeded from a closed session stream
+--EXTENSIONS--
+openssl
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_server_alpn_closed_parent.pem.tmp';
+include 'CertificateGenerator.inc';
+(new CertificateGenerator())->saveNewCertAsFileWithKey('localhost', $certFile);
+
+$server = stream_socket_server('tcp://127.0.0.1:0');
+$addr = stream_socket_get_name($server, false);
+
+$parentClient = stream_socket_client("tcp://$addr");
+$parent = stream_socket_accept($server);
+stream_context_set_options($parent, ['ssl' => [
+    'local_cert' => $certFile,
+    'alpn_protocols' => 'h2,http/1.1',
+]]);
+stream_set_blocking($parent, false);
+var_dump(stream_socket_enable_crypto($parent, true, STREAM_CRYPTO_METHOD_TLS_SERVER));
+
+$client = stream_socket_client("tcp://$addr", $errno, $errstr, 5, STREAM_CLIENT_CONNECT,
+    stream_context_create(['ssl' => [
+        'verify_peer' => false,
+        'verify_peer_name' => false,
+        'alpn_protocols' => 'h2',
+    ]]));
+$child = stream_socket_accept($server);
+stream_set_blocking($child, false);
+var_dump(stream_socket_enable_crypto($child, true, STREAM_CRYPTO_METHOD_TLS_SERVER, $parent));
+
+fclose($parent);
+fclose($parentClient);
+
+stream_set_blocking($client, false);
+$clientDone = $childDone = 0;
+for ($i = 0; $i < 1000 && ($clientDone !== true || $childDone !== true); $i++) {
+    if ($clientDone !== true) {
+        $clientDone = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+    }
+    if ($childDone !== true) {
+        $childDone = stream_socket_enable_crypto($child, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
+    }
+    usleep(1000);
+}
+var_dump($clientDone, $childDone);
+var_dump(stream_get_meta_data($client)['crypto']['alpn_protocol']);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_server_alpn_closed_parent.pem.tmp');
+?>
+--EXPECT--
+int(0)
+int(0)
+bool(true)
+bool(true)
+string(2) "h2"
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index f4f6a932efe..60cbf78ae71 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -785,7 +785,8 @@ static int php_openssl_win_cert_verify_callback(X509_STORE_CTX *x509_store_ctx,
 	bool is_self_signed = false;


-	stream = (php_stream*)arg;
+	SSL *ssl = X509_STORE_CTX_get_ex_data(x509_store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
+	stream = (php_stream*)SSL_get_ex_data(ssl, php_openssl_get_ssl_stream_data_index());
 	sslsock = (php_openssl_netstream_data_t*)stream->abstract;

 	{ /* First convert the x509 struct back to a DER encoded buffer and let Windows decode it into a form it can work with */
@@ -1020,7 +1021,7 @@ static zend_result php_openssl_enable_peer_verification(SSL_CTX *ctx, php_stream
 		}
 	} else {
 #ifdef PHP_WIN32
-		SSL_CTX_set_cert_verify_callback(ctx, php_openssl_win_cert_verify_callback, (void *)stream);
+		SSL_CTX_set_cert_verify_callback(ctx, php_openssl_win_cert_verify_callback, NULL);
 #else
 		if (sslsock->is_client && !SSL_CTX_set_default_verify_paths(ctx)) {
 			php_stream_warn(stream, ProtocolError,
@@ -1649,7 +1650,10 @@ static int php_openssl_server_alpn_callback(SSL *ssl_handle,
 		const unsigned char **out, unsigned char *outlen,
 		const unsigned char *in, unsigned int inlen, void *arg) /* {{{ */
 {
-	php_openssl_netstream_data_t *sslsock = arg;
+	php_stream *stream = (php_stream *)SSL_get_ex_data(ssl_handle, php_openssl_get_ssl_stream_data_index());
+	php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract;
+
+	ZEND_ASSERT(sslsock->alpn_ctx.data != NULL);

 	if (SSL_select_next_proto((unsigned char **)out, outlen, sslsock->alpn_ctx.data, sslsock->alpn_ctx.len, in, inlen) != OPENSSL_NPN_NEGOTIATED) {
 		return SSL_TLSEXT_ERR_NOACK;
@@ -2707,7 +2711,7 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream,
 			} else {
 				sslsock->alpn_ctx.data = (unsigned char *) pestrndup((const char*)alpn, alpn_len, php_stream_is_persistent(stream));
 				sslsock->alpn_ctx.len = alpn_len;
-				SSL_CTX_set_alpn_select_cb(sslsock->ctx, php_openssl_server_alpn_callback, sslsock);
+				SSL_CTX_set_alpn_select_cb(sslsock->ctx, php_openssl_server_alpn_callback, NULL);
 			}

 			efree(alpn);
@@ -2808,6 +2812,16 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
 		} else {
 			SSL_CTX_up_ref(parent_sslsock->ctx);
 			sslsock->ctx = parent_sslsock->ctx;
+#ifdef HAVE_TLS_ALPN
+			if (parent_sslsock->alpn_ctx.data) {
+				if (sslsock->alpn_ctx.data) {
+					pefree(sslsock->alpn_ctx.data, php_stream_is_persistent(stream));
+				}
+				sslsock->alpn_ctx.data = (unsigned char *) pestrndup((const char *) parent_sslsock->alpn_ctx.data,
+					parent_sslsock->alpn_ctx.len, php_stream_is_persistent(stream));
+				sslsock->alpn_ctx.len = parent_sslsock->alpn_ctx.len;
+			}
+#endif
 			if (parent_sslsock->session_callbacks) {
 				parent_sslsock->session_callbacks->refcount++;
 				sslsock->session_callbacks = parent_sslsock->session_callbacks;