From 34be2cf0b84f6bbee52df7d8190b052b995fc1ea Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 4 Nov 2026 18:29:54 -0500 Subject: [PATCH] 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. --- NEWS | 3 + ..._resumption_server_alpn_closed_parent.phpt | 59 +++++++++++++++++++ ext/openssl/xp_ssl.c | 22 +++++-- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 ext/openssl/tests/session_resumption_server_alpn_closed_parent.phpt diff --git a/NEWS b/NEWS index 300f44d70dd4..0fd53de990d7 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,9 @@ PHP NEWS - OpenSSL: . Fixed bug GH-25172 (TLS session reuse shares request-lifetime callback structures with persistent streams). (cxxz16, David Carlier) + . 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) 05 Nov 2026, PHP 8.6.0RC5 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 000000000000..f612fd5fbf98 --- /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-- +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-- + +--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 dcde68176b8b..bda11db924fc 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;