Commit 0efecbc4325 for php.net
commit 0efecbc432538a86dde4714b5d5cd7dbf212bc1f
Author: Ilija Tovilo <ilija.tovilo@me.com>
Date: Thu Jan 15 14:53:57 2026 +0100
Fix by-ref assignment to uninitialized hooked backing value
Within hooks, the backing value can directly be accessed as if no hooks were
present. This was previously handled only in read_property().
zend_fetch_property_address(), which is used for by-ref assignment, will first
call get_property_ptr_ptr() and then try read_property(). However, when called
on uninitialized backing values, read_property() will return
&EG(uninitialized_zval) with an uninitialized property warning. This is
problematic for zend_fetch_property_address() because it write to the result of
read_property() unless there's an exception.
For untyped properties, this can result in writes to &EG(uninitialized_zval)
(see oss-fuzz-471486164-001.phpt). For types properties, it will result in an
unexpected "Typed property C::$prop must not be accessed before initialization"
exception.
Fixes OSS-Fuzz #471486164
Closes GH-20943
diff --git a/NEWS b/NEWS
index 0c4c5da48b4..19f900235d8 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,8 @@ PHP NEWS
. Fixed bug GH-GH-20914 (Internal enums can be cloned and compared). (Arnaud)
. Fix OSS-Fuzz #474613951 (Leaked parent property default value). (ilutov)
. Fixed bug GH-20766 (Use-after-free in FE_FREE with GC interaction). (Bob)
+ . Fix OSS-Fuzz #471486164 (Broken by-ref assignment to uninitialized hooked
+ backing value). (ilutov)
- Date:
. Update timelib to 2022.16. (Derick)
diff --git a/Zend/tests/oss-fuzz-471486164-001.phpt b/Zend/tests/oss-fuzz-471486164-001.phpt
new file mode 100644
index 00000000000..a48a56398c1
--- /dev/null
+++ b/Zend/tests/oss-fuzz-471486164-001.phpt
@@ -0,0 +1,22 @@
+--TEST--
+OSS-Fuzz #471486164: get_property_ptr_ptr() on uninitialized hooked property
+--FILE--
+<?php
+
+class C {
+ public $a {
+ get => $this->a;
+ set { $this->a = &$value; }
+ }
+ public $x = 1;
+}
+
+$proxy = (new ReflectionClass(C::class))->newLazyProxy(function ($proxy) {
+ $proxy->a = 1;
+ return new C;
+});
+var_dump($proxy->x);
+
+?>
+--EXPECT--
+int(1)
diff --git a/Zend/tests/oss-fuzz-471486164-002.phpt b/Zend/tests/oss-fuzz-471486164-002.phpt
new file mode 100644
index 00000000000..688dd761220
--- /dev/null
+++ b/Zend/tests/oss-fuzz-471486164-002.phpt
@@ -0,0 +1,26 @@
+--TEST--
+OSS-Fuzz #471486164: get_property_ptr_ptr() on uninitialized hooked property
+--FILE--
+<?php
+
+class C {
+ public int $a {
+ get => $this->a;
+ set {
+ global $ref;
+ $this->a = &$ref;
+ }
+ }
+}
+
+$ref = 1;
+$proxy = new C;
+$proxy->a = 1;
+var_dump($proxy->a);
+$ref++;
+var_dump($proxy->a);
+
+?>
+--EXPECT--
+int(1)
+int(2)
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index c113f65a7a8..ccedb79acc0 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -1392,6 +1392,7 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
property_offset = zend_get_property_offset(zobj->ce, name, (zobj->ce->__get != NULL), cache_slot, &prop_info);
if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) {
+try_again:
retval = OBJ_PROP(zobj, property_offset);
if (UNEXPECTED(Z_TYPE_P(retval) == IS_UNDEF)) {
if (EXPECTED(!zobj->ce->__get) ||
@@ -1471,7 +1472,15 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
}
retval = zend_hash_add(zobj->properties, name, &EG(uninitialized_zval));
}
- } else if (!IS_HOOKED_PROPERTY_OFFSET(property_offset) && zobj->ce->__get == NULL) {
+ } else if (IS_HOOKED_PROPERTY_OFFSET(property_offset)) {
+ if (!(prop_info->flags & ZEND_ACC_VIRTUAL) && !zend_should_call_hook(prop_info, zobj)) {
+ property_offset = prop_info->offset;
+ if (!ZEND_TYPE_IS_SET(prop_info->type)) {
+ prop_info = NULL;
+ }
+ goto try_again;
+ }
+ } else if (zobj->ce->__get == NULL) {
retval = &EG(error_zval);
}