Commit 943720bb2ad for php.net
commit 943720bb2ad1bcbb961e419c5aaee3fc57082e8e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed May 13 14:54:43 2026 -0400
ext/openssl: Throw on repeat Openssl\Session::__unserialize
Calling __unserialize() on an Openssl\Session that already holds an
SSL_SESSION overwrote obj->session without freeing the prior one, leaking
an SSL_SESSION (OpenSSL's allocator, bypasses memory_limit). There is no
legitimate use for re-running __unserialize() on a live instance, so
reject it: throw when obj->session is already populated, before any
overwrite.
Closes GH-22040
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index e7f29c6a354..2db966c1661 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -504,6 +504,12 @@ PHP_METHOD(Openssl_Session, __unserialize)
Z_PARAM_ARRAY_HT(data)
ZEND_PARSE_PARAMETERS_END();
+ php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS);
+ if (obj->session != NULL) {
+ zend_throw_error(NULL, "Cannot call Openssl\\Session::__unserialize() on an already-initialized session");
+ RETURN_THROWS();
+ }
+
zval *pem_zv = zend_hash_str_find(data, ZEND_STRL("pem"));
if (!pem_zv || Z_TYPE_P(pem_zv) != IS_STRING) {
zend_throw_exception(php_openssl_exception_ce, "Invalid serialization data", 0);
@@ -524,7 +530,6 @@ PHP_METHOD(Openssl_Session, __unserialize)
RETURN_THROWS();
}
- php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS);
obj->session = session;
/* Populate id property */
diff --git a/ext/openssl/tests/session_unserialize_repeat.phpt b/ext/openssl/tests/session_unserialize_repeat.phpt
new file mode 100644
index 00000000000..b1e33c2d03b
--- /dev/null
+++ b/ext/openssl/tests/session_unserialize_repeat.phpt
@@ -0,0 +1,84 @@
+--TEST--
+Openssl\Session::__unserialize throws on a repeat call
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_unserialize_repeat.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $serverCtx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ ]]);
+
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx);
+ phpt_notify_server_start($server);
+
+ $client = @stream_socket_accept($server, 10);
+ if ($client) {
+ fwrite($client, "ok\n");
+ fclose($client);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $captured = null;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function ($s, $session) use (&$captured) {
+ $captured = $session;
+ return true;
+ },
+ 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
+ ]]);
+
+ $c = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 10,
+ STREAM_CLIENT_CONNECT, $ctx);
+ if (!$c) {
+ echo "connect failed: $errstr\n";
+ return;
+ }
+ fread($c, 8);
+ fclose($c);
+
+ if (!$captured instanceof Openssl\Session) {
+ echo "no session captured\n";
+ return;
+ }
+
+ $payload = $captured->__serialize();
+ $sess = unserialize(serialize($captured));
+ echo "first: " . (is_object($sess) ? get_class($sess) : "fail") . "\n";
+
+ try {
+ $sess->__unserialize($payload);
+ echo "second: no throw\n";
+ } catch (Error $e) {
+ echo "second: " . $e->getMessage() . "\n";
+ }
+
+ echo "alive\n";
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session-unserialize-repeat', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_unserialize_repeat.pem.tmp');
+?>
+--EXPECTF--
+first: Openssl\Session
+second: Cannot call Openssl\Session::__unserialize() on an already-initialized session
+alive