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);
 	}