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. */
}
/* }}} */