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