Commit ca26066659a for php

commit ca26066659ad0deef8a6ffad86021b1a01b4051b
Author: lazerg <lazerg2@gmail.com>
Date:   Sun Nov 8 16:02:39 2026 +0000

    Fix GH-25299: Exception string code is lost after unserialize()

    Exception::__wakeup() unsets $code when it is not an int, so a PDOException
    with a SQLSTATE code like "HY000" comes back from unserialize() with no code
    at all, and getCode() warns and returns null. PDOException sets string codes
    itself, and $code is untyped, so the check now keeps strings too. Other types
    are still dropped as before.

    Message and code are also dereferenced before the type check, so values held
    by reference are no longer dropped.

    Co-authored-by: David Carlier <devnexen@gmail.com>

    Close GH-25301

diff --git a/NEWS b/NEWS
index 8dc0c27ef24..9ae8c7add12 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,8 @@ PHP                                                                        NEWS
     (Jorg Sowa)
   . Fixed bug GH-25265 (Misaligned read for virtual internal props in
     hooked subclass). (Lazizbek Ergashev)
+  . Fixed bug GH-25299 (Exception string code is lost after unserialize()).
+    (Lazizbek Ergashev, David Carlier)

 - Curl:
   . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
diff --git a/Zend/tests/gh25299.phpt b/Zend/tests/gh25299.phpt
new file mode 100644
index 00000000000..b1fe486b0f5
--- /dev/null
+++ b/Zend/tests/gh25299.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GH-25299 (Exception string code is lost after unserialize())
+--CREDITS--
+tacman
+--FILE--
+<?php
+
+class MyException extends Exception {
+    protected $code = 'HY000';
+}
+
+$e = unserialize(serialize(new MyException()));
+var_dump($e->getCode());
+
+$e = unserialize('O:9:"Exception":1:{s:7:"' . "\0*\0" . 'code";a:0:{}}');
+var_dump($e->getCode());
+
+?>
+--EXPECTF--
+string(5) "HY000"
+
+Warning: Undefined property: Exception::$code in %s on line %d
+NULL
diff --git a/Zend/tests/gh25299_ref.phpt b/Zend/tests/gh25299_ref.phpt
new file mode 100644
index 00000000000..c4e0d84a821
--- /dev/null
+++ b/Zend/tests/gh25299_ref.phpt
@@ -0,0 +1,22 @@
+--TEST--
+GH-25299: Exception::__wakeup() keeps message and code held by reference
+--FILE--
+<?php
+
+class MyException extends Exception {
+    public $m = 'm';
+    public $c = 5;
+
+    public function __construct() {
+        $this->message = &$this->m;
+        $this->code = &$this->c;
+    }
+}
+
+$e = unserialize(serialize(new MyException()));
+var_dump($e->getMessage(), $e->getCode());
+
+?>
+--EXPECT--
+string(1) "m"
+int(5)
diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c
index 6138e302bb5..e6ec1bf90ee 100644
--- a/Zend/zend_exceptions.c
+++ b/Zend/zend_exceptions.c
@@ -347,9 +347,10 @@ ZEND_METHOD(Exception, __construct)
 /* }}} */

 /* {{{ Exception unserialize checks */
-#define CHECK_EXC_TYPE(id, type) \
+#define CHECK_EXC_TYPE(id, type_mask) \
 	pvalue = zend_read_property_ex(i_get_exception_base(Z_OBJ_P(object)), Z_OBJ_P(object), ZSTR_KNOWN(id), 1, &value); \
-	if (Z_TYPE_P(pvalue) != IS_NULL && Z_TYPE_P(pvalue) != type) { \
+	ZVAL_DEREF(pvalue); \
+	if (Z_TYPE_P(pvalue) != IS_NULL && !((type_mask) & (1u << Z_TYPE_P(pvalue)))) { \
 		zend_unset_property(i_get_exception_base(Z_OBJ_P(object)), Z_OBJ_P(object), ZSTR_VAL(ZSTR_KNOWN(id)), ZSTR_LEN(ZSTR_KNOWN(id))); \
 	}

@@ -359,8 +360,8 @@ ZEND_METHOD(Exception, __wakeup)

 	zval value, *pvalue;
 	zval *object = ZEND_THIS;
-	CHECK_EXC_TYPE(ZEND_STR_MESSAGE, IS_STRING);
-	CHECK_EXC_TYPE(ZEND_STR_CODE,    IS_LONG);
+	CHECK_EXC_TYPE(ZEND_STR_MESSAGE, MAY_BE_STRING);
+	CHECK_EXC_TYPE(ZEND_STR_CODE,    MAY_BE_LONG | MAY_BE_STRING);
 	/* The type of all other properties is enforced through typed properties. */
 }
 /* }}} */