Skip to content

Commit 86a2c5b

Browse files
committed
Fix GH-21478: Forward property operations to real instance for initialized lazy proxies
1 parent 9f33bff commit 86a2c5b

File tree

10 files changed

+214
-4
lines changed

10 files changed

+214
-4
lines changed

Zend/tests/lazy_objects/gh18038-002.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,4 @@ var_dump($obj->prop);
3434
--EXPECT--
3535
init
3636
string(19) "RealInstance::__set"
37-
string(12) "Proxy::__set"
3837
int(2)

Zend/tests/lazy_objects/gh18038-004.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ var_dump($real->prop);
3636
--EXPECTF--
3737
init
3838
string(19) "RealInstance::__get"
39-
string(12) "Proxy::__get"
4039

4140
Warning: Undefined property: RealInstance::$prop in %s on line %d
4241
NULL

Zend/tests/lazy_objects/gh18038-007.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ var_dump(isset($real->prop['']));
3636
--EXPECT--
3737
init
3838
string(21) "RealInstance::__isset"
39-
string(14) "Proxy::__isset"
4039
bool(false)
4140
bool(false)

Zend/tests/lazy_objects/gh18038-009.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ var_dump(isset($real->prop));
3636
--EXPECT--
3737
init
3838
string(21) "RealInstance::__isset"
39-
string(14) "Proxy::__isset"
4039
bool(false)
4140
bool(false)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
GH-21478: __isset on lazy proxy should not double-invoke when real instance guard is set
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public $_;
8+
9+
public function __isset($name) {
10+
global $proxy;
11+
printf("__isset(\$%s) on %s\n", $name, $this::class);
12+
return isset($proxy->{$name});
13+
}
14+
}
15+
16+
class Bar extends Foo {}
17+
18+
$rc = new ReflectionClass(Bar::class);
19+
$proxy = $rc->newLazyProxy(function () {
20+
echo "Init\n";
21+
return new Foo();
22+
});
23+
24+
$real = $rc->initializeLazyObject($proxy);
25+
isset($real->x);
26+
27+
?>
28+
--EXPECT--
29+
Init
30+
__isset($x) on Foo
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
GH-21478: Proxy's own __get runs when accessed directly (not from real instance)
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
private $_;
8+
9+
public function __get($name) {
10+
echo __CLASS__, " ", $name, "\n";
11+
}
12+
}
13+
14+
class Bar extends Foo {
15+
public function __get($name) {
16+
echo __CLASS__, " ", $name, "\n";
17+
}
18+
}
19+
20+
$rc = new ReflectionClass(Bar::class);
21+
$proxy = $rc->newLazyProxy(function () {
22+
return new Foo();
23+
});
24+
$rc->initializeLazyObject($proxy);
25+
26+
$proxy->x;
27+
28+
?>
29+
--EXPECT--
30+
Bar x
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
GH-21478: __set on lazy proxy should not double-invoke when real instance guard is set
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class Foo {
8+
public $_;
9+
10+
public function __set($name, $value) {
11+
global $proxy;
12+
printf("__set(\$%s) on %s\n", $name, $this::class);
13+
$proxy->{$name} = $value;
14+
}
15+
}
16+
17+
#[AllowDynamicProperties]
18+
class Bar extends Foo {}
19+
20+
$rc = new ReflectionClass(Bar::class);
21+
$proxy = $rc->newLazyProxy(function () {
22+
echo "Init\n";
23+
return new Foo();
24+
});
25+
26+
$real = $rc->initializeLazyObject($proxy);
27+
$real->x = 1;
28+
29+
?>
30+
--EXPECT--
31+
Init
32+
__set($x) on Foo
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
GH-21478: __unset on lazy proxy should not double-invoke when real instance guard is set
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public $_;
8+
9+
public function __unset($name) {
10+
global $proxy;
11+
printf("__unset(\$%s) on %s\n", $name, $this::class);
12+
unset($proxy->{$name});
13+
}
14+
}
15+
16+
class Bar extends Foo {}
17+
18+
$rc = new ReflectionClass(Bar::class);
19+
$proxy = $rc->newLazyProxy(function () {
20+
echo "Init\n";
21+
return new Foo();
22+
});
23+
24+
$real = $rc->initializeLazyObject($proxy);
25+
unset($real->x);
26+
27+
?>
28+
--EXPECT--
29+
Init
30+
__unset($x) on Foo
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
GH-21478 (Property access on lazy proxy may invoke magic method despite real instance guards)
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public $_;
8+
9+
public function __get($name) {
10+
global $proxy;
11+
printf("__get(\$%s) on %s\n", $name, $this::class);
12+
return $proxy->{$name};
13+
}
14+
}
15+
16+
class Bar extends Foo {}
17+
18+
$rc = new ReflectionClass(Bar::class);
19+
$proxy = $rc->newLazyProxy(function () {
20+
echo "Init\n";
21+
return new Foo();
22+
});
23+
24+
$real = $rc->initializeLazyObject($proxy);
25+
$real->x;
26+
27+
?>
28+
--EXPECTF--
29+
Init
30+
__get($x) on Foo
31+
32+
Warning: Undefined property: Foo::$x in %s on line %d

Zend/zend_object_handlers.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,23 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
886886

887887
retval = &EG(uninitialized_zval);
888888

889+
/* For initialized lazy proxies: if the real instance's magic method
890+
* guard is already set for this property, we are inside a recursive
891+
* call from the real instance's __get/__isset. Forward directly to
892+
* the real instance to avoid double invocation. (GH-21478) */
893+
if (UNEXPECTED(zend_object_is_lazy_proxy(zobj)
894+
&& zend_lazy_object_initialized(zobj))) {
895+
zend_object *instance = zend_lazy_object_get_instance(zobj);
896+
if (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS) {
897+
uint32_t *instance_guard = zend_get_property_guard(instance, name);
898+
uint32_t guard_type = ((type == BP_VAR_IS) && zobj->ce->__isset)
899+
? IN_ISSET : IN_GET;
900+
if ((*instance_guard) & guard_type) {
901+
return zend_std_read_property(instance, name, type, cache_slot, rv);
902+
}
903+
}
904+
}
905+
889906
/* magic isset */
890907
if ((type == BP_VAR_IS) && zobj->ce->__isset) {
891908
zval tmp_result;
@@ -1203,6 +1220,20 @@ found:;
12031220
goto exit;
12041221
}
12051222

1223+
/* For initialized lazy proxies: if the real instance's __set guard
1224+
* is already set, we are inside a recursive call from the real
1225+
* instance's __set. Forward directly to avoid double invocation. */
1226+
if (UNEXPECTED(zend_object_is_lazy_proxy(zobj)
1227+
&& zend_lazy_object_initialized(zobj))) {
1228+
zend_object *instance = zend_lazy_object_get_instance(zobj);
1229+
if (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS) {
1230+
uint32_t *instance_guard = zend_get_property_guard(instance, name);
1231+
if ((*instance_guard) & IN_SET) {
1232+
return zend_std_write_property(instance, name, value, cache_slot);
1233+
}
1234+
}
1235+
}
1236+
12061237
/* magic set */
12071238
if (zobj->ce->__set) {
12081239
if (!guard) {
@@ -1595,6 +1626,21 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
15951626
return;
15961627
}
15971628

1629+
/* For initialized lazy proxies: if the real instance's __unset guard
1630+
* is already set, we are inside a recursive call from the real
1631+
* instance's __unset. Forward directly to avoid double invocation. */
1632+
if (UNEXPECTED(zend_object_is_lazy_proxy(zobj)
1633+
&& zend_lazy_object_initialized(zobj))) {
1634+
zend_object *instance = zend_lazy_object_get_instance(zobj);
1635+
if (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS) {
1636+
uint32_t *instance_guard = zend_get_property_guard(instance, name);
1637+
if ((*instance_guard) & IN_UNSET) {
1638+
zend_std_unset_property(instance, name, cache_slot);
1639+
return;
1640+
}
1641+
}
1642+
}
1643+
15981644
/* magic unset */
15991645
if (zobj->ce->__unset) {
16001646
if (!guard) {
@@ -2412,6 +2458,20 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has
24122458
goto exit;
24132459
}
24142460

2461+
/* For initialized lazy proxies: if the real instance's __isset guard
2462+
* is already set, we are inside a recursive call from the real
2463+
* instance's __isset. Forward directly to avoid double invocation. */
2464+
if (UNEXPECTED(zend_object_is_lazy_proxy(zobj)
2465+
&& zend_lazy_object_initialized(zobj))) {
2466+
zend_object *instance = zend_lazy_object_get_instance(zobj);
2467+
if (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS) {
2468+
uint32_t *instance_guard = zend_get_property_guard(instance, name);
2469+
if ((*instance_guard) & IN_ISSET) {
2470+
return zend_std_has_property(instance, name, has_set_exists, cache_slot);
2471+
}
2472+
}
2473+
}
2474+
24152475
if (!zobj->ce->__isset) {
24162476
goto lazy_init;
24172477
}

0 commit comments

Comments
 (0)