Commit ae42ecf1f34 for php.net

commit ae42ecf1f349034b77b89da1b607098de15e66bf
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jul 31 07:50:45 2026 -0400

    Resolve the effective property in ReflectionProperty lazy APIs

    skipLazyInitialization() and isLazy() indexed the object with the
    property info of the reflected scope. When a child adds hooks to a plain
    parent property the child gets its own storage slot, so a parent-scoped
    ReflectionProperty read and wrote the dead parent slot: isLazy() reported
    false and skipLazyInitialization() was a silent no-op. Resolve the
    effective property first, as getRawValue(), setRawValue() and
    setRawValueWithoutLazyInitialization() already do.

    Closes GH-22969

diff --git a/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt b/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt
new file mode 100644
index 00000000000..615b9c2bb5c
--- /dev/null
+++ b/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt
@@ -0,0 +1,48 @@
+--TEST--
+Lazy Objects: ReflectionProperty::isLazy() with a hooked override
+--FILE--
+<?php
+
+class B {
+    public $plain = 1;
+    public $virtual { get => 1; }
+    private $priv = 1;
+    public static $stat;
+}
+
+class C extends B {
+    public $plain { get => $this->plain; set => $this->plain = $value; }
+    public $priv = 2;
+}
+
+$reflector = new ReflectionClass(C::class);
+
+foreach (['Ghost', 'Proxy'] as $kind) {
+    printf("# %s:\n", $kind);
+
+    $obj = $kind === 'Ghost'
+        ? $reflector->newLazyGhost(function ($obj) { })
+        : $reflector->newLazyProxy(function ($obj) { return new C(); });
+
+    foreach (['plain', 'virtual', 'stat'] as $name) {
+        printf("%s: B scope %d, C scope %d\n", $name,
+            (new ReflectionProperty(B::class, $name))->isLazy($obj),
+            (new ReflectionProperty(C::class, $name))->isLazy($obj));
+    }
+
+    printf("priv: B scope %d\n",
+        (new ReflectionProperty(B::class, 'priv'))->isLazy($obj));
+}
+
+?>
+--EXPECT--
+# Ghost:
+plain: B scope 1, C scope 1
+virtual: B scope 0, C scope 0
+stat: B scope 0, C scope 0
+priv: B scope 1
+# Proxy:
+plain: B scope 1, C scope 1
+virtual: B scope 0, C scope 0
+stat: B scope 0, C scope 0
+priv: B scope 1
diff --git a/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt b/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt
new file mode 100644
index 00000000000..818bf17915d
--- /dev/null
+++ b/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Lazy Objects: ReflectionProperty::skipLazyInitialization() with a hooked override
+--FILE--
+<?php
+
+class B {
+    public $b = 1;
+}
+
+class C extends B {
+    public $b { get => $this->b; set => $this->b = $value; }
+}
+
+$reflector = new ReflectionClass(C::class);
+
+foreach (['Ghost', 'Proxy'] as $kind) {
+    printf("# %s:\n", $kind);
+
+    $obj = $kind === 'Ghost'
+        ? $reflector->newLazyGhost(function ($obj) { $obj->b = 9; })
+        : $reflector->newLazyProxy(function ($obj) { $c = new C(); $c->b = 9; return $c; });
+
+    (new ReflectionProperty(B::class, 'b'))->skipLazyInitialization($obj);
+
+    printf("is lazy: %d\n", $reflector->isUninitializedLazyObject($obj));
+    var_dump($obj->b);
+}
+
+?>
+--EXPECT--
+# Ghost:
+is lazy: 0
+NULL
+# Proxy:
+is lazy: 0
+NULL
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index f153cf83e14..34ea93305bc 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -6365,19 +6365,22 @@ ZEND_METHOD(ReflectionProperty, skipLazyInitialization)
 		Z_PARAM_OBJ_OF_CLASS(object, intern->ce)
 	} ZEND_PARSE_PARAMETERS_END();

-	if (reflection_property_check_lazy_compatible(ref->prop,
-				ref->unmangled_name, intern, object,
-				"skipLazyInitialization") == FAILURE) {
-		RETURN_THROWS();
-	}
-
 	while (zend_object_is_lazy_proxy(object)
 			&& zend_lazy_object_initialized(object)) {
 		object = zend_lazy_object_get_instance(object);
 	}

-	zval *src = &object->ce->default_properties_table[OBJ_PROP_TO_NUM(ref->prop->offset)];
-	zval *dst = OBJ_PROP(object, ref->prop->offset);
+	zend_property_info *prop = reflection_property_get_effective_prop(ref,
+			intern->ce, object);
+
+	if (reflection_property_check_lazy_compatible(prop,
+				ref->unmangled_name, intern, object,
+				"skipLazyInitialization") == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	zval *src = &object->ce->default_properties_table[OBJ_PROP_TO_NUM(prop->offset)];
+	zval *dst = OBJ_PROP(object, prop->offset);

 	if (!(Z_PROP_FLAG_P(dst) & IS_PROP_LAZY)) {
 		/* skipLazyInitialization has no effect on non-lazy properties */
@@ -6409,16 +6412,19 @@ ZEND_METHOD(ReflectionProperty, isLazy)
 		Z_PARAM_OBJ_OF_CLASS(object, intern->ce)
 	} ZEND_PARSE_PARAMETERS_END();

-	if (!ref->prop || ref->prop->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) {
-		RETURN_FALSE;
-	}
-
 	while (zend_object_is_lazy_proxy(object)
 			&& zend_lazy_object_initialized(object)) {
 		object = zend_lazy_object_get_instance(object);
 	}

-	RETURN_BOOL(Z_PROP_FLAG_P(OBJ_PROP(object, ref->prop->offset)) & IS_PROP_LAZY);
+	const zend_property_info *prop = reflection_property_get_effective_prop(ref,
+			intern->ce, object);
+
+	if (!prop || prop->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) {
+		RETURN_FALSE;
+	}
+
+	RETURN_BOOL(Z_PROP_FLAG_P(OBJ_PROP(object, prop->offset)) & IS_PROP_LAZY);
 }

 /* {{{ Returns true if property was initialized */