From 87d364d3e4490d51cdc849ebb399cb716cd307bd Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 19 Oct 2026 08:26:05 -0400 Subject: [PATCH] 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. --- NEWS | 2 + ext/date/tests/unserialize-test.phpt | 3 -- .../serialize/__unserialize_malformed.phpt | 38 +++++++++++++++++++ ...serialize_malformed_nested_destructor.phpt | 27 +++++++++++++ .../tests/serialize/__wakeup_malformed.phpt | 38 +++++++++++++++++++ ext/standard/var_unserializer.re | 12 +++--- 6 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 ext/standard/tests/serialize/__unserialize_malformed.phpt create mode 100644 ext/standard/tests/serialize/__unserialize_malformed_nested_destructor.phpt create mode 100644 ext/standard/tests/serialize/__wakeup_malformed.phpt diff --git a/NEWS b/NEWS index 5ea9ecbafb0b..47d477609333 100644 --- a/NEWS +++ b/NEWS @@ -160,6 +160,8 @@ PHP NEWS (Lazizbek Ergashev) - Standard: + . 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 aaff75db6b77..6096d7b7288e 100644 --- a/ext/date/tests/unserialize-test.phpt +++ b/ext/date/tests/unserialize-test.phpt @@ -31,7 +31,6 @@ bool(false) 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 @@ bool(false) 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 000000000000..6d43767e20f5 --- /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-- + '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 000000000000..94ae62bb0970 --- /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-- + '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 000000000000..b8cceead72a8 --- /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-- +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 e5cc1acd758c..01092119b2a9 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)