Commit e60f0c28a15 for php
commit e60f0c28a15dd30e7144f9a15522578da47c38f8
Author: Daniel Scherzer <daniel.e.scherzer+phpf@gmail.com>
Date: Thu Oct 1 19:53:50 2026 -0700
ext/openssl: fix use after free when `early_data_cb` callback closes stream
diff --git a/ext/openssl/tests/close_in_early_data_cb_UAF.phpt b/ext/openssl/tests/close_in_early_data_cb_UAF.phpt
new file mode 100644
index 00000000000..9f47600d284
--- /dev/null
+++ b/ext/openssl/tests/close_in_early_data_cb_UAF.phpt
@@ -0,0 +1,93 @@
+--TEST--
+Closing the stream in the `early_data_cb` callback triggers use after free
+--EXTENSIONS--
+openssl
+--CREDITS--
+Calif.io
+--CLEAN--
+<?php
+
+$certFile = sys_get_temp_dir() . '/php-audit-early-data-' . getmypid() . '.pem';
+@unlink($certFile);
+?>
+--FILE--
+<?php
+
+$certFile = sys_get_temp_dir() . '/php-audit-early-data-' . getmypid() . '.pem';
+
+$serverCode = <<<'PHP'
+$context = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'audit-early-data',
+ 'max_early_data' => 16384,
+ 'early_data_cb' => function ($stream, string $data): void {
+ fclose($stream);
+ },
+]]);
+
+$server = stream_socket_server(
+ 'tls://127.0.0.1:0',
+ $errno,
+ $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
+ $context,
+);
+phpt_notify_server_start($server);
+
+for ($connection = 0; $connection < 2; $connection++) {
+ $client = @stream_socket_accept($server, 10);
+ if ($client) {
+ fwrite($client, "response\n");
+ fclose($client);
+ }
+}
+PHP;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'PHP'
+$session = null;
+$context = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function ($stream, $newSession) use (&$session): void {
+ $session = $newSession;
+ },
+]]);
+
+$first = stream_socket_client(
+ 'tls://{{ ADDR }}',
+ $errno,
+ $errstr,
+ 10,
+ STREAM_CLIENT_CONNECT,
+ $context,
+);
+fgets($first);
+fclose($first);
+
+$earlyDataContext = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_data' => $session,
+ 'early_data' => 'attacker-controlled-early-data',
+]]);
+
+@stream_socket_client(
+ 'tls://{{ ADDR }}',
+ $errno,
+ $errstr,
+ 10,
+ STREAM_CLIENT_CONNECT,
+ $earlyDataContext,
+);
+PHP;
+
+require 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('audit_early_data', $certFile);
+
+require 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+
+?>
+--EXPECT--
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 60cbf78ae71..1df29d007e0 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -2997,8 +2997,13 @@ static int php_openssl_handshake_server_early_data(php_stream *stream,
zval args[2];
ZVAL_RES(&args[0], stream->res);
ZVAL_STRINGL(&args[1], (char *) buf, readbytes);
+ bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
+ stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
zend_call_known_fcc(&sslsock->early_data_callbacks->read_cb,
NULL, 2, args, NULL);
+ if (!originally_no_fclose) {
+ stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE;
+ }
zval_ptr_dtor(&args[1]);
}
continue;