From 5f404e2cbc42ab52992cffe324b4182d05109d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 10 Sep 2026 09:24:12 +0200 Subject: [PATCH 1/5] zend_API: Verify property types in `object_properties_load()` Fixes php/php-src#9708. --- UPGRADING.INTERNALS | 2 ++ Zend/zend_API.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 31995eb80752..d13e40b12d11 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -182,6 +182,8 @@ PHP 8.6 INTERNALS UPGRADE NOTES instead of a zval*. Accordingly, zend_get_closure_this_ptr() now returns that zend_object*, or NULL when the closure is unbound, instead of a zval* that is IS_UNDEF when the closure is unbound. + . object_properties_load() now verifies that the given value is assignable + to typed properties. The check is performed in non-strict mode. - Added: . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 43e21cafd564..3b4c49535b16 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1794,6 +1794,10 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope return; } } + if (ZEND_TYPE_IS_SET(property_info->type) && !zend_verify_property_type(property_info, prop, /* strict */ false)) { + return; + } + zval_ptr_dtor(slot); ZVAL_COPY_VALUE(slot, prop); zval_add_ref(slot); From e531c2deb8e7c78f75cd1e721d37534c1c028e74 Mon Sep 17 00:00:00 2001 From: Nora Dossche <7771979+ndossche@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:27:19 +0200 Subject: [PATCH 2/5] zend_API: Fix various issues in `object_properties_load()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes Fixes php/php-src#9707. Co-authored-by: Tim Düsterhus --- Zend/zend_API.c | 52 +++++++++++++++---- .../ArrayObject/gh_9707_unserialize.phpt | 41 +++++++++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 ext/spl/tests/ArrayObject/gh_9707_unserialize.phpt diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 3b4c49535b16..72fa3c634ae1 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1758,7 +1758,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope zval *prop, tmp; zend_string *key; zend_long h; - const zend_property_info *property_info; + zend_property_info *property_info; ZEND_HASH_FOREACH_KEY_VAL(properties, h, key, prop) { if (key) { @@ -1785,22 +1785,52 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope if (property_info != ZEND_WRONG_PROPERTY_INFO && property_info && (property_info->flags & ZEND_ACC_STATIC) == 0) { + bool is_typed = ZEND_TYPE_IS_SET(property_info->type); + + /* Mimick unserialize behaviour for virtual properties. */ + if (UNEXPECTED(property_info->flags & ZEND_ACC_VIRTUAL)) { + zend_throw_error(NULL, "Cannot unserialize value for virtual property %s::$%s", ZSTR_VAL(object->ce->name), zend_get_unmangled_property_name(property_info->name)); + return; + } + zval *slot = OBJ_PROP(object, property_info->offset); - if (UNEXPECTED((property_info->flags & ZEND_ACC_READONLY) && !Z_ISUNDEF_P(slot))) { - if (Z_PROP_FLAG_P(slot) & IS_PROP_REINITABLE) { - Z_PROP_FLAG_P(slot) &= ~IS_PROP_REINITABLE; + + /* Mimick zend_assign_to_typed_prop() by reporting the error before doing work. */ + if (UNEXPECTED((property_info->flags & ZEND_ACC_READONLY) + && !Z_ISUNDEF_P(slot) + && !(Z_PROP_FLAG_P(slot) & IS_PROP_REINITABLE))) { + zend_readonly_property_modification_error(property_info); + return; + } + + zval val; + + if (is_typed) { + if (UNEXPECTED(Z_ISREF_P(prop))) { + if (UNEXPECTED(!zend_verify_prop_assignable_by_ref(property_info, prop, /* strict */ false))) { + ZEND_ASSERT(EG(exception)); + return; + } + ZVAL_COPY(&val, prop); + ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(&val), property_info); } else { - zend_readonly_property_modification_error(property_info); - return; + ZVAL_COPY(&val, prop); + if (UNEXPECTED(!zend_verify_property_type(property_info, &val, /* strict */ false))) { + zval_ptr_dtor(&val); + return; + } } - } - if (ZEND_TYPE_IS_SET(property_info->type) && !zend_verify_property_type(property_info, prop, /* strict */ false)) { - return; + if (UNEXPECTED(Z_ISREF_P(slot)) + && (ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(slot)))) { + ZEND_REF_DEL_TYPE_SOURCE(Z_REF_P(slot), property_info); + } + } else { + ZVAL_COPY(&val, prop); } + Z_PROP_FLAG_P(slot) &= ~IS_PROP_REINITABLE; zval_ptr_dtor(slot); - ZVAL_COPY_VALUE(slot, prop); - zval_add_ref(slot); + ZVAL_COPY_VALUE(slot, &val); if (object->properties) { ZVAL_INDIRECT(&tmp, slot); zend_hash_update(object->properties, key, &tmp); diff --git a/ext/spl/tests/ArrayObject/gh_9707_unserialize.phpt b/ext/spl/tests/ArrayObject/gh_9707_unserialize.phpt new file mode 100644 index 000000000000..ee11c5c28e5c --- /dev/null +++ b/ext/spl/tests/ArrayObject/gh_9707_unserialize.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-9707: object_properties_load crashes in debug mode when unserializing references to typed properties in php 8.1+ +--FILE-- +a; +$f->b = &$r; + +$f->b = 1; +var_dump($unserialized = unserialize(serialize($f))); + +$unserialized->b = 2; + +var_dump($unserialized); + +?> +--EXPECTF-- +object(Foo)#%d (3) { + ["a"]=> + &int(1) + ["b"]=> + &int(1) + ["storage":"ArrayObject":private]=> + array(0) { + } +} +object(Foo)#%d (3) { + ["a"]=> + &int(2) + ["b"]=> + &int(2) + ["storage":"ArrayObject":private]=> + array(0) { + } +} From 0ddaa688fe10904d00a5c0273473e8f5755c09e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 11 Sep 2026 10:47:04 +0200 Subject: [PATCH 3/5] NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 4efc8258fc2d..68c96424a9ce 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ PHP NEWS - Core: . Fixed incorrect internal pointer and foreach iterator positions when compacting arrays with holes. (Weilin Du) + . Fix handling of references to typed properties during unserialization + of various internal classes. (ndossche, timwolla) - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function From 8be11973a93d53049e053bef49b62b8c2c0cba31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 10 Sep 2026 09:40:10 +0200 Subject: [PATCH 4/5] random: Remove now-obsolete manual `$engine` type check in `Randomizer::__unserialize()` --- ext/random/randomizer.c | 8 +------- .../tests/03_randomizer/gh_9708_unserialize.phpt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 ext/random/tests/03_randomizer/gh_9708_unserialize.phpt diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 0738380ca253..4c5ff34c9fea 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -508,7 +508,6 @@ PHP_METHOD(Random_Randomizer, __unserialize) php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); HashTable *d; zval *members_zv; - zval *zengine; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ARRAY_HT(d); @@ -531,12 +530,7 @@ PHP_METHOD(Random_Randomizer, __unserialize) RETURN_THROWS(); } - zengine = zend_read_property(randomizer->std.ce, &randomizer->std, "engine", strlen("engine"), 1, NULL); - if (Z_TYPE_P(zengine) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(zengine), random_ce_Random_Engine)) { - zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0); - RETURN_THROWS(); - } - + zval *zengine = zend_read_property(randomizer->std.ce, &randomizer->std, "engine", strlen("engine"), /* silent */ true, NULL); randomizer_common_init(randomizer, Z_OBJ_P(zengine)); } /* }}} */ diff --git a/ext/random/tests/03_randomizer/gh_9708_unserialize.phpt b/ext/random/tests/03_randomizer/gh_9708_unserialize.phpt new file mode 100644 index 000000000000..c5689d7b6b94 --- /dev/null +++ b/ext/random/tests/03_randomizer/gh_9708_unserialize.phpt @@ -0,0 +1,14 @@ +--TEST-- +GH-9708: object_properties_load() bypasses typed property checks +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +Exception: Invalid serialization data for Random\Randomizer object From d4f001252fe526b3909ecfa3ebb922b1e55fec9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 11 Sep 2026 12:53:53 +0200 Subject: [PATCH 5/5] zend_API: Make `object_properties_load()` use `strict_types=1` This is consistent with regular unserialization, which also performs strict type checking. Co-authored-by: Gina Peter Banyard --- UPGRADING.INTERNALS | 2 +- Zend/zend_API.c | 4 ++-- .../ArrayObject/gh_9708_unserialize.phpt | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 ext/spl/tests/ArrayObject/gh_9708_unserialize.phpt diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index d13e40b12d11..8bbdc5caabe5 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -183,7 +183,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES that zend_object*, or NULL when the closure is unbound, instead of a zval* that is IS_UNDEF when the closure is unbound. . object_properties_load() now verifies that the given value is assignable - to typed properties. The check is performed in non-strict mode. + to typed properties. The check is performed in strict mode. - Added: . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 72fa3c634ae1..7e35b30058e2 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1807,7 +1807,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope if (is_typed) { if (UNEXPECTED(Z_ISREF_P(prop))) { - if (UNEXPECTED(!zend_verify_prop_assignable_by_ref(property_info, prop, /* strict */ false))) { + if (UNEXPECTED(!zend_verify_prop_assignable_by_ref(property_info, prop, /* strict */ true))) { ZEND_ASSERT(EG(exception)); return; } @@ -1815,7 +1815,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(&val), property_info); } else { ZVAL_COPY(&val, prop); - if (UNEXPECTED(!zend_verify_property_type(property_info, &val, /* strict */ false))) { + if (UNEXPECTED(!zend_verify_property_type(property_info, &val, /* strict */ true))) { zval_ptr_dtor(&val); return; } diff --git a/ext/spl/tests/ArrayObject/gh_9708_unserialize.phpt b/ext/spl/tests/ArrayObject/gh_9708_unserialize.phpt new file mode 100644 index 000000000000..fec7ef60346c --- /dev/null +++ b/ext/spl/tests/ArrayObject/gh_9708_unserialize.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-9708: object_properties_load() bypasses typed property checks +--FILE-- +getMessage(), "\n"; +} + + +?> +--EXPECT-- +TypeError: Cannot assign string to property Foo::$a of type int