From 98468bc2df361d81279ddb1ac68698ffbb8ed81d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 1 Nov 2026 17:22:19 +0100 Subject: [PATCH] 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. --- ..._resumption_remove_cb_closed_listener.phpt | 138 ++++++++++++++++++ ext/openssl/xp_ssl.c | 4 + 2 files changed, 142 insertions(+) create mode 100644 ext/openssl/tests/session_resumption_remove_cb_closed_listener.phpt 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 000000000000..05476a6ae375 --- /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-- + +--FILE-- + [ + '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-- + +--EXPECT-- +server alive diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 5cead037715a..cf7a2802ee16 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; }