Commit 2b99297b764 for php
commit 2b99297b76457100e1688e49697bb0e6a5124327
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Oct 19 08:26:05 2026 -0400
ext/standard: Validate objects before queuing unserialize callbacks
Check the closing brace before queuing __unserialize() or __wakeup().
Incomplete object records must use the same cleanup as invalid property
data, including destructor suppression. The __unserialize() data array of
a failed record now stays in the unserialize context until var_destroy()
instead of being released at once, so a destructor of a nested object
cannot resolve a back-reference to an element the array already released.
Closes GH-24433.
diff --git a/NEWS b/NEWS
index 14df7132c7c..a654cec2f39 100644
--- a/NEWS
+++ b/NEWS
@@ -248,6 +248,8 @@ PHP NEWS
- Standard:
. Fixed memory leaks in checkdnsrr() and dns_check_record() on Windows.
(Ilia Alshanetsky)
+ . Fixed __unserialize() and __wakeup() being called for objects with a missing
+ closing brace. (Ilia Alshanetsky)
. Fixed bug GH-24308 (Request shutdown calls closelog() even when syslog
was not opened by PHP). (lazerg)
. Fixed bug GH-24399 (usleep() and time_nanosleep() ignore queued APCs on
diff --git a/ext/date/tests/unserialize-test.phpt b/ext/date/tests/unserialize-test.phpt
index aaff75db6b7..6096d7b7288 100644
--- a/ext/date/tests/unserialize-test.phpt
+++ b/ext/date/tests/unserialize-test.phpt
@@ -31,7 +31,6 @@
ossfuzz-55599.txt:
%s: unserialize(): Error at offset 26 of 26 bytes in %sunserialize-test.php on line %d
-Error: Invalid serialization data for DateTime object
bool(false)
@@ -42,6 +41,4 @@
ossfuzz-56931.txt:
%s: unserialize(): Error at offset 39 of 39 bytes in %sunserialize-test.php on line %d
-
-Deprecated: Creation of dynamic property DateInterval::$ is deprecated in %s on line %d
bool(false)
diff --git a/ext/standard/tests/serialize/__unserialize_malformed.phpt b/ext/standard/tests/serialize/__unserialize_malformed.phpt
new file mode 100644
index 00000000000..6d43767e20f
--- /dev/null
+++ b/ext/standard/tests/serialize/__unserialize_malformed.phpt
@@ -0,0 +1,38 @@
+--TEST--
+__unserialize() and destructors are not called for incomplete objects
+--FILE--
+<?php
+class Test {
+ public function __unserialize(array $data): void {
+ echo "__unserialize: ";
+ var_dump($data);
+ }
+
+ public function __destruct() {
+ echo "__destruct\n";
+ }
+}
+
+foreach ([
+ 'empty' => 'O:4:"Test":0:{',
+ 'nonempty' => 'O:4:"Test":1:{i:0;i:42;',
+ 'nested' => 'a:1:{i:0;O:4:"Test":0:{',
+] as $name => $data) {
+ echo "$name: ";
+ var_dump(@unserialize($data));
+}
+
+echo "valid: ";
+var_dump(unserialize('O:4:"Test":1:{i:0;i:42;}'));
+?>
+--EXPECT--
+empty: bool(false)
+nonempty: bool(false)
+nested: bool(false)
+valid: __unserialize: array(1) {
+ [0]=>
+ int(42)
+}
+object(Test)#1 (0) {
+}
+__destruct
diff --git a/ext/standard/tests/serialize/__unserialize_malformed_nested_destructor.phpt b/ext/standard/tests/serialize/__unserialize_malformed_nested_destructor.phpt
new file mode 100644
index 00000000000..94ae62bb097
--- /dev/null
+++ b/ext/standard/tests/serialize/__unserialize_malformed_nested_destructor.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Destructor of a nested object in a failed __unserialize() payload cannot reach freed slots
+--FILE--
+<?php
+class T {
+ public function __unserialize(array $data): void {}
+}
+
+class D {
+ public function __destruct() {
+ var_dump(unserialize('R:2;'));
+ }
+}
+
+foreach ([
+ 'missing brace' => 'O:1:"T":2:{i:0;s:5:"hello";i:1;O:1:"D":0:{}',
+ 'malformed element' => 'O:1:"T":3:{i:0;s:5:"hello";i:1;O:1:"D":0:{}i:2;X',
+] as $name => $data) {
+ echo "$name: ";
+ var_dump(@unserialize($data));
+}
+?>
+--EXPECT--
+missing brace: bool(false)
+bool(false)
+malformed element: bool(false)
+bool(false)
diff --git a/ext/standard/tests/serialize/__wakeup_malformed.phpt b/ext/standard/tests/serialize/__wakeup_malformed.phpt
new file mode 100644
index 00000000000..b8cceead72a
--- /dev/null
+++ b/ext/standard/tests/serialize/__wakeup_malformed.phpt
@@ -0,0 +1,38 @@
+--TEST--
+__wakeup() and destructors are not called for incomplete objects
+--FILE--
+<?php
+class Test {
+ public int $value = 0;
+
+ public function __wakeup(): void {
+ echo "__wakeup: $this->value\n";
+ }
+
+ public function __destruct() {
+ echo "__destruct\n";
+ }
+}
+
+foreach ([
+ 'empty' => 'O:4:"Test":0:{',
+ 'nonempty' => 'O:4:"Test":1:{s:5:"value";i:42;',
+ 'nested' => 'a:1:{i:0;O:4:"Test":0:{',
+] as $name => $data) {
+ echo "$name: ";
+ var_dump(@unserialize($data));
+}
+
+echo "valid: ";
+var_dump(unserialize('O:4:"Test":1:{s:5:"value";i:42;}'));
+?>
+--EXPECT--
+empty: bool(false)
+nonempty: bool(false)
+nested: bool(false)
+valid: __wakeup: 42
+object(Test)#1 (1) {
+ ["value"]=>
+ int(42)
+}
+__destruct
diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index e5cc1acd758..01092119b2a 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -855,10 +855,11 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
if (UNEXPECTED(!array_finit_size_mixed(&ary, elements))) {
return 0;
}
- if (!process_nested_array_data(UNSERIALIZE_PASSTHRU, Z_ARRVAL(ary), elements)) {
+ if (!process_nested_array_data(UNSERIALIZE_PASSTHRU, Z_ARRVAL(ary), elements)
+ || !finish_nested_data(UNSERIALIZE_PASSTHRU)) {
ZVAL_DEREF(rval);
GC_ADD_FLAGS(Z_OBJ_P(rval), IS_OBJ_DESTRUCTOR_CALLED);
- zval_ptr_dtor(&ary);
+ ZVAL_COPY_VALUE(var_tmp_var(var_hash), &ary);
return 0;
}
@@ -871,7 +872,7 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
tmp++;
ZVAL_COPY_VALUE(tmp, &ary);
- return finish_nested_data(UNSERIALIZE_PASSTHRU);
+ return 1;
}
has_wakeup = Z_OBJCE_P(rval) != PHP_IC_ENTRY
@@ -885,7 +886,8 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
if (UNEXPECTED(zend_hash_fextend(ht, zend_hash_num_elements(ht) + elements, HT_IS_PACKED(ht)) == FAILURE)) {
return 0;
}
- if (!process_nested_object_data(UNSERIALIZE_PASSTHRU, ht, elements, Z_OBJ_P(rval))) {
+ if (!process_nested_object_data(UNSERIALIZE_PASSTHRU, ht, elements, Z_OBJ_P(rval))
+ || !finish_nested_data(UNSERIALIZE_PASSTHRU)) {
if (has_wakeup) {
ZVAL_DEREF(rval);
GC_ADD_FLAGS(Z_OBJ_P(rval), IS_OBJ_DESTRUCTOR_CALLED);
@@ -901,7 +903,7 @@ static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, bool
Z_EXTRA_P(wakeup_var) = VAR_WAKEUP_FLAG;
}
- return finish_nested_data(UNSERIALIZE_PASSTHRU);
+ return 1;
}
#ifdef PHP_WIN32
# pragma optimize("", on)