Commit 7e86f2c4433 for php

commit 7e86f2c443382b2f03a546b51ca065872454c087
Author: Jakub Zelenka <bukka@php.net>
Date:   Sun Nov 1 17:22:19 2026 +0100

    openssl: Fix use-after-free in TLS session remove callback

    The session remove callback reads the listener stream from the SSL_CTX
    ex_data, but the context is shared with accepted connections and can
    outlive the listener. Closing the listener left a dangling pointer that
    the callback later dereferenced. Clear it on close when it refers to the
    stream being freed.

    The added test reproduces it via a TCP proxy and crashes under ASan
    without the fix.

    Closes GH-25132

diff --git a/NEWS b/NEWS
index 20daec0d497..2f148c6d54c 100644
--- a/NEWS
+++ b/NEWS
@@ -145,6 +145,8 @@ PHP                                                                        NEWS
   . Fixed build with OpenSSL 4.1. (Weilin Du)
   . Fixed bug GH-24784 (openssl_pkcs7_read() and openssl_cms_read() overwrite
     certificates with CRLs). (Alexander Danilov)
+  . Fixed use-after-free in the TLS session remove callback when the listener
+    stream is closed while a shared context is still in use. (Jakub Zelenka)

 - PDO_Firebird:
   . Fixed bug GH-25006 (getColumnMeta() reads out of bounds for an
diff --git a/ext/openssl/tests/session_resumption_remove_cb_closed_listener.phpt b/ext/openssl/tests/session_resumption_remove_cb_closed_listener.phpt
new file mode 100644
index 00000000000..05476a6ae37
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_remove_cb_closed_listener.phpt
@@ -0,0 +1,138 @@
+--TEST--
+TLS session resumption - session_remove_cb with a closed listener stream
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+if (substr(PHP_OS, 0, 3) == 'WIN') die("skip not reliable on Windows");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_remove_cb_closed_listener.pem.tmp';
+
+/* A TCP proxy relays the handshake, then injects a bad record so the server
+ * removes the cached session after the listener has been closed. */
+
+$serverCode = <<<'CODE'
+    $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+    $ctx = stream_context_create(['ssl' => [
+        'local_cert' => '%s',
+        'verify_peer' => false,
+        'no_ticket' => true,
+        'session_cache' => true,
+        'session_id_context' => 'app',
+        'session_new_cb'    => function($stream, $session) {},
+        'session_get_cb'    => function($stream, $sessionId) { return null; },
+        'session_remove_cb' => function($stream, $sessionId) {},
+    ]]);
+
+    $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+    phpt_notify_server_start($server);
+
+    // Handshake completes during accept, so the session is cached here.
+    $client = @stream_socket_accept($server, 30);
+
+    // Free the listener while the accepted connection keeps the ctx alive.
+    fclose($server);
+    phpt_notify();
+
+    // Reuse the freed listener's memory to make a dangling access fault.
+    $reuse = [];
+    for ($i = 0; $i < 8; $i++) {
+        $reuse[] = fopen('php://memory', 'r+');
+    }
+
+    @fread($client, 1024);
+    phpt_notify(message: "server alive");
+    fclose($client);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$proxyCode = <<<'CODE'
+    $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+    $proxy = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, $flags);
+    phpt_notify_server_start($proxy);
+
+    $upstream = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT);
+    $conn = stream_socket_accept($proxy, 30);
+    stream_set_blocking($upstream, false);
+    stream_set_blocking($conn, false);
+    stream_set_blocking(STDIN, false);
+
+    $injected = false;
+    while (true) {
+        $r = [$conn, $upstream, STDIN];
+        $w = $e = [];
+        if (@stream_select($r, $w, $e, 10) === false) {
+            break;
+        }
+        foreach ($r as $s) {
+            if ($s === STDIN) {
+                if (fgets(STDIN) === false) {
+                    break 2;
+                }
+                if (!$injected) {
+                    $injected = true;
+                    fwrite($upstream, "\x15\x03\x03\x00\x20" . str_repeat("\x41", 0x20));
+                }
+            } elseif ($s === $conn) {
+                $d = @fread($conn, 8192);
+                if ($d === '' || $d === false) {
+                    if (feof($conn)) break 2;
+                } else {
+                    @fwrite($upstream, $d);
+                }
+            } else {
+                $d = @fread($upstream, 8192);
+                if ($d === '' || $d === false) {
+                    if (feof($upstream)) break 2;
+                } else {
+                    @fwrite($conn, $d);
+                }
+            }
+        }
+    }
+    fclose($conn);
+    fclose($upstream);
+    fclose($proxy);
+CODE;
+
+$clientCode = <<<'CODE'
+    $flags = STREAM_CLIENT_CONNECT;
+    $ctx = stream_context_create(['ssl' => [
+        'verify_peer' => false,
+        'verify_peer_name' => false,
+    ]]);
+
+    $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+    if (!$client) {
+        echo "client connect failed: $errstr\n";
+        phpt_notify('proxy');
+        return;
+    }
+
+    // Listener is closed; tell the proxy to inject the bad record.
+    phpt_wait('server');
+    phpt_notify('proxy');
+
+    echo trim(phpt_wait('server')) . "\n";
+    fclose($client);
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_remove_cb_closed_listener', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, [
+    'server' => $serverCode,
+    'proxy'  => $proxyCode,
+]);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_remove_cb_closed_listener.pem.tmp');
+?>
+--EXPECT--
+server alive
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 5cead037715..cf7a2802ee1 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -3376,6 +3376,10 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{
 			sslsock->ssl_handle = NULL;
 		}
 		if (sslsock->ctx) {
+			/* Clear the ctx stream pointer as the ctx may outlive this stream. */
+			if (SSL_CTX_get_ex_data(sslsock->ctx, php_openssl_get_ctx_stream_data_index()) == stream) {
+				SSL_CTX_set_ex_data(sslsock->ctx, php_openssl_get_ctx_stream_data_index(), NULL);
+			}
 			SSL_CTX_free(sslsock->ctx);
 			sslsock->ctx = NULL;
 		}