From 77dff333411d867cd90c82e6a0a39eefc2a26bea Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 1 Oct 2026 19:53:50 -0700 Subject: [PATCH 1/5] ext/openssl: fix use after free when `early_data_cb` callback closes stream --- .../tests/close_in_early_data_cb_UAF.phpt | 93 +++++++++++++++++++ ext/openssl/xp_ssl.c | 5 + 2 files changed, 98 insertions(+) create mode 100644 ext/openssl/tests/close_in_early_data_cb_UAF.phpt 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 000000000000..9f47600d2841 --- /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-- + +--FILE-- + [ + '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 706f4ec5c80f..07c4398af9b6 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2952,8 +2952,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; From 23118bfff6c6b4c54c66a4fb3ef212d81aa6f987 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 1 Oct 2026 19:58:04 -0700 Subject: [PATCH 2/5] ext/openssl: fix use after free when `psk_client_cb` callback closes stream --- .../tests/close_in_psk_client_cb_UAF.phpt | 61 +++++++++++++++++++ ext/openssl/xp_ssl.c | 8 +++ 2 files changed, 69 insertions(+) create mode 100644 ext/openssl/tests/close_in_psk_client_cb_UAF.phpt diff --git a/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt b/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt new file mode 100644 index 000000000000..d5d96c9b0b90 --- /dev/null +++ b/ext/openssl/tests/close_in_psk_client_cb_UAF.phpt @@ -0,0 +1,61 @@ +--TEST-- +Closing the stream in the `psk_client_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--FILE-- + [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, + 'ciphers' => 'PSK', + 'psk_server_cb' => static function ($stream, string $identity): ?Openssl\Psk { + if ($identity !== 'audit-client') { + return null; + } + + return new Openssl\Psk(str_repeat("\x41", 32)); + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +@stream_socket_accept($server, 10); +PHP; + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'ciphers' => 'PSK', + 'verify_peer' => false, + 'verify_peer_name' => false, + 'psk_client_cb' => static function ($stream): ?Openssl\Psk { + fclose($stream); + + return new Openssl\Psk(str_repeat("\x41", 32), 'audit-client'); + }, +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +PHP; + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 07c4398af9b6..ab7cb6fecd93 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1788,11 +1788,19 @@ static unsigned int php_openssl_psk_client_cb(SSL *ssl, const char *hint, return 0; } + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zval result; if (php_openssl_call_psk_cb(stream, &sslsock->psk_callbacks->client_cb, NULL, 0, &result) != SUCCESS) { + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } return 0; } + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } if (Z_TYPE(result) == IS_NULL) { return 0; From c89d7cb689001e9350d85049068b7f61c9357a1a Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 1 Oct 2026 19:49:07 -0700 Subject: [PATCH 3/5] ext/openssl: fix use after free when `psk_server_cb` callback closes stream --- .../tests/close_in_psk_server_cb_UAF.phpt | 55 +++++++++++++++++++ ext/openssl/xp_ssl.c | 8 +++ 2 files changed, 63 insertions(+) create mode 100644 ext/openssl/tests/close_in_psk_server_cb_UAF.phpt diff --git a/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt b/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt new file mode 100644 index 000000000000..3a561ef877e8 --- /dev/null +++ b/ext/openssl/tests/close_in_psk_server_cb_UAF.phpt @@ -0,0 +1,55 @@ +--TEST-- +Closing the stream in the `psk_server_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--FILE-- + [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, + 'psk_server_cb' => function ($stream, string $identity): ?Openssl\Psk { + fclose($stream); + return null; + }, +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +@stream_socket_accept($server, 10); +PHP; + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'psk_client_cb' => fn ($stream) => new Openssl\Psk( + str_repeat("\x42", 32), + 'remote-identity', + ), +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +PHP; + +require 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + +?> +--EXPECT-- diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index ab7cb6fecd93..c09540f364dd 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1970,11 +1970,19 @@ static int php_openssl_psk_find_session_cb(SSL *ssl, const unsigned char *identi return 1; } + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zval result; if (php_openssl_call_psk_cb(stream, &sslsock->psk_callbacks->server_cb, identity, identity_len, &result) != SUCCESS) { + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } return 0; } + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } if (Z_TYPE(result) == IS_NULL) { return 1; /* identity unknown - let handshake fall through */ From 3e8ff57ddc9c5bc0bb49066f7255ae0eb048790c Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 1 Oct 2026 20:02:38 -0700 Subject: [PATCH 4/5] ext/openssl: fix use after free when `session_get_cb` callback closes stream --- .../tests/close_in_session_get_cb_UAF.phpt | 103 ++++++++++++++++++ ext/openssl/xp_ssl.c | 5 + 2 files changed, 108 insertions(+) create mode 100644 ext/openssl/tests/close_in_session_get_cb_UAF.phpt diff --git a/ext/openssl/tests/close_in_session_get_cb_UAF.phpt b/ext/openssl/tests/close_in_session_get_cb_UAF.phpt new file mode 100644 index 000000000000..edf926c19c19 --- /dev/null +++ b/ext/openssl/tests/close_in_session_get_cb_UAF.phpt @@ -0,0 +1,103 @@ +--TEST-- +Closing the stream in the `session_get_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--CLEAN-- + +--FILE-- + [ + 'local_cert' => '%s', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, + 'session_id_context' => 'audit-session-get', + 'session_new_cb' => static function ($stream, $session) use (&$sessions): void { + $sessions[bin2hex($session->id)] = $session; + }, + 'session_get_cb' => static function ($stream, string $sessionId) use (&$sessions) { + $session = $sessions[bin2hex($sessionId)] ?? null; + fclose($stream); + + return $session; + }, +]]); + +$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 ($i = 0; $i < 2; $i++) { + $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' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => static function ($stream, $newSession) use (&$session): void { + $session = $newSession; + }, +]]); + +$first = stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +if ($first) { + @fgets($first); + fclose($first); +} + +$resumeContext = stream_context_create(['ssl' => [ + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $session, +]]); + +@stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $resumeContext, +); +PHP; + +require 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('audit_session_get', $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 c09540f364dd..32c59fa303c7 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2249,7 +2249,12 @@ static SSL_SESSION *php_openssl_session_get_cb(SSL *ssl, const unsigned char *se SSL_SESSION *session = NULL; + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zend_call_known_fcc(&sslsock->session_callbacks->get_cb, &retval, 2, args, NULL); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } zval_ptr_dtor(&args[1]); if (php_openssl_is_session_ce(&retval)) { From 3c15c17821235845644bf2df94f382f1661ff60e Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 1 Oct 2026 20:05:09 -0700 Subject: [PATCH 5/5] ext/openssl: fix use after free when `session_new_cb` callback closes stream --- .../tests/close_in_session_new_cb_UAF.phpt | 71 +++++++++++++++++++ ext/openssl/xp_ssl.c | 5 ++ 2 files changed, 76 insertions(+) create mode 100644 ext/openssl/tests/close_in_session_new_cb_UAF.phpt diff --git a/ext/openssl/tests/close_in_session_new_cb_UAF.phpt b/ext/openssl/tests/close_in_session_new_cb_UAF.phpt new file mode 100644 index 000000000000..1b088f6711f6 --- /dev/null +++ b/ext/openssl/tests/close_in_session_new_cb_UAF.phpt @@ -0,0 +1,71 @@ +--TEST-- +Closing the stream in the `session_new_cb` callback triggers use after free +--EXTENSIONS-- +openssl +--CREDITS-- +Calif.io +--CLEAN-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => true, + 'session_id_context' => 'audit-session', +]]); + +$server = stream_socket_server( + 'tls://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context, +); +phpt_notify_server_start($server); +$client = @stream_socket_accept($server, 10); +if ($client) { + fwrite($client, "ticket\n"); + fclose($client); +} +PHP; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'PHP' +$context = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function ($stream, $session): void { + fclose($stream); + }, +]]); + +$client = @stream_socket_client( + 'tls://{{ ADDR }}', + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context, +); +if ($client) { + @fgets($client); +} +PHP; + +require 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('audit_session', $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 32c59fa303c7..57f6f2c763c9 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2216,7 +2216,12 @@ static int php_openssl_session_new_cb(SSL *ssl, SSL_SESSION *session) ZVAL_RES(&args[0], stream->res); php_openssl_session_object_init(&args[1], session); + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zend_call_known_fcc(&sslsock->session_callbacks->new_cb, NULL, 2, args, NULL); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } zval_ptr_dtor(&args[1]);