Skip to content

Commit 6ab5a5b

Browse files
committedMar 27, 2025··
Merge branch 'PHP-8.4'
* PHP-8.4: Fix lazy proxy calling magic methods twice
2 parents 3e8677e + 26f5009 commit 6ab5a5b

13 files changed

+502
-21
lines changed
 
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
GH-18038 001: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __set($name, $value) {
10+
var_dump(__METHOD__);
11+
$this->$name = $value * 2;
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyProxy(function () {
18+
echo "init\n";
19+
return new C;
20+
});
21+
22+
$obj->prop = 1;
23+
var_dump($obj->prop);
24+
25+
?>
26+
--EXPECT--
27+
string(8) "C::__set"
28+
init
29+
int(2)
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
GH-18038 002: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class RealInstance {
8+
public $_;
9+
public function __set($name, $value) {
10+
global $obj;
11+
var_dump(get_class($this)."::".__FUNCTION__);
12+
$obj->$name = $value * 2;
13+
unset($this->$name);
14+
$this->$name = $value * 2;
15+
}
16+
}
17+
18+
#[AllowDynamicProperties]
19+
class Proxy extends RealInstance {
20+
}
21+
22+
$rc = new ReflectionClass(Proxy::class);
23+
24+
$obj = $rc->newLazyProxy(function () {
25+
echo "init\n";
26+
return new RealInstance;
27+
});
28+
29+
$real = $rc->initializeLazyObject($obj);
30+
$real->prop = 1;
31+
var_dump($obj->prop);
32+
33+
?>
34+
--EXPECT--
35+
init
36+
string(19) "RealInstance::__set"
37+
string(12) "Proxy::__set"
38+
int(2)
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
GH-18038 003: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __get($name) {
10+
var_dump(__METHOD__);
11+
return $this->$name;
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyProxy(function () {
18+
echo "init\n";
19+
return new C;
20+
});
21+
22+
var_dump($obj->prop);
23+
24+
?>
25+
--EXPECTF--
26+
string(8) "C::__get"
27+
init
28+
29+
Warning: Undefined property: C::$prop in %s on line %d
30+
NULL
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
GH-18038 004: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class RealInstance {
8+
public $_;
9+
public function __get($name) {
10+
global $obj;
11+
var_dump(get_class($this)."::".__FUNCTION__);
12+
var_dump($obj->$name);
13+
return $this->$name;
14+
}
15+
}
16+
17+
#[AllowDynamicProperties]
18+
class Proxy extends RealInstance {
19+
public function __get($name) {
20+
var_dump(get_class($this)."::".__FUNCTION__);
21+
return $this->$name;
22+
}
23+
}
24+
25+
$rc = new ReflectionClass(Proxy::class);
26+
27+
$obj = $rc->newLazyProxy(function () {
28+
echo "init\n";
29+
return new RealInstance;
30+
});
31+
32+
$real = $rc->initializeLazyObject($obj);
33+
var_dump($real->prop);
34+
35+
?>
36+
--EXPECTF--
37+
init
38+
string(19) "RealInstance::__get"
39+
string(12) "Proxy::__get"
40+
41+
Warning: Undefined property: RealInstance::$prop in %s on line %d
42+
NULL
43+
44+
Warning: Undefined property: RealInstance::$prop in %s on line %d
45+
NULL
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
GH-18038 005: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __isset($name) {
10+
var_dump(__METHOD__);
11+
return isset($this->$name['']);
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyProxy(function () {
18+
echo "init\n";
19+
return new C;
20+
});
21+
22+
var_dump(isset($obj->prop['']));
23+
24+
?>
25+
--EXPECT--
26+
string(10) "C::__isset"
27+
init
28+
bool(false)
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
GH-18038 006: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __isset($name) {
10+
var_dump(__METHOD__);
11+
return isset($this->$name['']);
12+
}
13+
public function __get($name) {
14+
var_dump(__METHOD__);
15+
return $this->$name[''];
16+
}
17+
}
18+
19+
$rc = new ReflectionClass(C::class);
20+
21+
$obj = $rc->newLazyProxy(function () {
22+
echo "init\n";
23+
return new C;
24+
});
25+
26+
var_dump(isset($obj->prop['']));
27+
28+
?>
29+
--EXPECTF--
30+
string(10) "C::__isset"
31+
string(8) "C::__get"
32+
init
33+
34+
Warning: Undefined property: C::$prop in %s on line %d
35+
36+
Warning: Trying to access array offset on null in %s on line %d
37+
bool(false)
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
GH-18038 007: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class RealInstance {
8+
public $_;
9+
public function __isset($name) {
10+
global $obj;
11+
var_dump(get_class($this)."::".__FUNCTION__);
12+
var_dump(isset($obj->$name['']));
13+
return isset($this->$name['']);
14+
}
15+
}
16+
17+
#[AllowDynamicProperties]
18+
class Proxy extends RealInstance {
19+
public function __isset($name) {
20+
var_dump(get_class($this)."::".__FUNCTION__);
21+
return isset($this->$name['']);
22+
}
23+
}
24+
25+
$rc = new ReflectionClass(Proxy::class);
26+
27+
$obj = $rc->newLazyProxy(function () {
28+
echo "init\n";
29+
return new RealInstance;
30+
});
31+
32+
$real = $rc->initializeLazyObject($obj);
33+
var_dump(isset($real->prop['']));
34+
35+
?>
36+
--EXPECT--
37+
init
38+
string(21) "RealInstance::__isset"
39+
string(14) "Proxy::__isset"
40+
bool(false)
41+
bool(false)
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
GH-18038 008: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __isset($name) {
10+
var_dump(__METHOD__);
11+
return isset($this->$name);
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyProxy(function () {
18+
echo "init\n";
19+
return new C;
20+
});
21+
22+
var_dump(isset($obj->prop));
23+
24+
?>
25+
--EXPECT--
26+
string(10) "C::__isset"
27+
init
28+
bool(false)
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
GH-18038 009: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class RealInstance {
8+
public $_;
9+
public function __isset($name) {
10+
global $obj;
11+
var_dump(get_class($this)."::".__FUNCTION__);
12+
var_dump(isset($obj->$name));
13+
return isset($this->$name);
14+
}
15+
}
16+
17+
#[AllowDynamicProperties]
18+
class Proxy extends RealInstance {
19+
public function __isset($name) {
20+
var_dump(get_class($this)."::".__FUNCTION__);
21+
return isset($this->$name);
22+
}
23+
}
24+
25+
$rc = new ReflectionClass(Proxy::class);
26+
27+
$obj = $rc->newLazyProxy(function () {
28+
echo "init\n";
29+
return new RealInstance;
30+
});
31+
32+
$real = $rc->initializeLazyObject($obj);
33+
var_dump(isset($real->prop));
34+
35+
?>
36+
--EXPECT--
37+
init
38+
string(21) "RealInstance::__isset"
39+
string(14) "Proxy::__isset"
40+
bool(false)
41+
bool(false)
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
GH-18038 010: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __unset($name) {
10+
var_dump(__METHOD__);
11+
unset($this->$name);
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyProxy(function () {
18+
echo "init\n";
19+
return new C;
20+
});
21+
22+
unset($obj->prop);
23+
var_dump($obj);
24+
25+
?>
26+
--EXPECTF--
27+
string(10) "C::__unset"
28+
init
29+
lazy proxy object(C)#%d (1) {
30+
["instance"]=>
31+
object(C)#%d (1) {
32+
["_"]=>
33+
NULL
34+
}
35+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
GH-18038 011: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class RealInstance {
8+
public $_;
9+
public function __unset($name) {
10+
global $obj;
11+
var_dump(get_class($this)."::".__FUNCTION__);
12+
unset($this->$name);
13+
}
14+
}
15+
16+
#[AllowDynamicProperties]
17+
class Proxy extends RealInstance {
18+
public function __isset($name) {
19+
var_dump(get_class($this)."::".__FUNCTION__);
20+
unset($this->$name);
21+
}
22+
}
23+
24+
$rc = new ReflectionClass(Proxy::class);
25+
26+
$obj = $rc->newLazyProxy(function () {
27+
echo "init\n";
28+
return new RealInstance;
29+
});
30+
31+
$real = $rc->initializeLazyObject($obj);
32+
unset($real->prop);
33+
var_dump($obj);
34+
35+
?>
36+
--EXPECTF--
37+
init
38+
string(21) "RealInstance::__unset"
39+
lazy proxy object(Proxy)#%d (1) {
40+
["instance"]=>
41+
object(RealInstance)#%d (1) {
42+
["_"]=>
43+
NULL
44+
}
45+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
GH-18038 012: Lazy proxy calls magic methods twice
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class C {
8+
public $_;
9+
public function __set($name, $value) {
10+
var_dump(__METHOD__);
11+
$this->$name = $value * 2;
12+
}
13+
}
14+
15+
$rc = new ReflectionClass(C::class);
16+
17+
$obj = $rc->newLazyGhost(function () {
18+
echo "init\n";
19+
});
20+
21+
$obj->prop = 1;
22+
var_dump($obj->prop);
23+
24+
?>
25+
--EXPECT--
26+
string(8) "C::__set"
27+
init
28+
int(2)

‎Zend/zend_object_handlers.c

+77-21
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,18 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
946946
goto exit;
947947
}
948948

949+
if (UNEXPECTED(guard)) {
950+
uint32_t guard_type = (type == BP_VAR_IS) && zobj->ce->__isset
951+
? IN_ISSET : IN_GET;
952+
guard = zend_get_property_guard(zobj, name);
953+
if (!((*guard) & guard_type)) {
954+
(*guard) |= guard_type;
955+
retval = zend_std_read_property(zobj, name, type, cache_slot, rv);
956+
(*guard) &= ~guard_type;
957+
return retval;
958+
}
959+
}
960+
949961
return zend_std_read_property(zobj, name, type, cache_slot, rv);
950962
}
951963
}
@@ -970,6 +982,43 @@ static zend_always_inline bool property_uses_strict_types(void) {
970982
&& ZEND_CALL_USES_STRICT_TYPES(EG(current_execute_data));
971983
}
972984

985+
static zval *forward_write_to_lazy_object(zend_object *zobj,
986+
zend_string *name, zval *value, void **cache_slot, bool guarded)
987+
{
988+
zval *variable_ptr;
989+
990+
/* backup value as it may change during initialization */
991+
zval backup;
992+
ZVAL_COPY(&backup, value);
993+
994+
zend_object *instance = zend_lazy_object_init(zobj);
995+
if (UNEXPECTED(!instance)) {
996+
zval_ptr_dtor(&backup);
997+
return &EG(error_zval);
998+
}
999+
1000+
if (UNEXPECTED(guarded)) {
1001+
uint32_t *guard = zend_get_property_guard(instance, name);
1002+
if (!((*guard) & IN_SET)) {
1003+
(*guard) |= IN_SET;
1004+
variable_ptr = zend_std_write_property(instance, name, &backup, cache_slot);
1005+
(*guard) &= ~IN_SET;
1006+
goto exit;
1007+
}
1008+
}
1009+
1010+
variable_ptr = zend_std_write_property(instance, name, &backup, cache_slot);
1011+
1012+
exit:
1013+
zval_ptr_dtor(&backup);
1014+
1015+
if (variable_ptr == &backup) {
1016+
variable_ptr = value;
1017+
}
1018+
1019+
return variable_ptr;
1020+
}
1021+
9731022
ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zval *value, void **cache_slot) /* {{{ */
9741023
{
9751024
zval *variable_ptr, tmp;
@@ -1151,7 +1200,8 @@ found:;
11511200
variable_ptr = value;
11521201
} else if (EXPECTED(!IS_WRONG_PROPERTY_OFFSET(property_offset))) {
11531202
if (UNEXPECTED(zend_lazy_object_must_init(zobj))) {
1154-
goto lazy_init;
1203+
return forward_write_to_lazy_object(zobj, name, value,
1204+
cache_slot, /* guarded */ true);
11551205
}
11561206

11571207
goto write_std_property;
@@ -1198,26 +1248,9 @@ found:;
11981248
exit:
11991249
return variable_ptr;
12001250

1201-
lazy_init:;
1202-
/* backup value as it may change during initialization */
1203-
zval backup;
1204-
ZVAL_COPY(&backup, value);
1205-
1206-
zobj = zend_lazy_object_init(zobj);
1207-
if (UNEXPECTED(!zobj)) {
1208-
variable_ptr = &EG(error_zval);
1209-
zval_ptr_dtor(&backup);
1210-
goto exit;
1211-
}
1212-
1213-
variable_ptr = zend_std_write_property(zobj, name, &backup, cache_slot);
1214-
zval_ptr_dtor(&backup);
1215-
1216-
if (variable_ptr == &backup) {
1217-
variable_ptr = value;
1218-
}
1219-
1220-
return variable_ptr;
1251+
lazy_init:
1252+
return forward_write_to_lazy_object(zobj, name, value, cache_slot,
1253+
/* guarded */ false);
12211254
}
12221255
/* }}} */
12231256

@@ -1538,6 +1571,17 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
15381571
if (!zobj) {
15391572
return;
15401573
}
1574+
1575+
if (UNEXPECTED(guard)) {
1576+
guard = zend_get_property_guard(zobj, name);
1577+
if (!((*guard) & IN_UNSET)) {
1578+
(*guard) |= IN_UNSET;
1579+
zend_std_unset_property(zobj, name, cache_slot);
1580+
(*guard) &= ~IN_UNSET;
1581+
return;
1582+
}
1583+
}
1584+
15411585
zend_std_unset_property(zobj, name, cache_slot);
15421586
return;
15431587
}
@@ -2327,6 +2371,8 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has
23272371
}
23282372
(*guard) &= ~IN_ISSET;
23292373
OBJ_RELEASE(zobj);
2374+
} else {
2375+
goto lazy_init;
23302376
}
23312377
}
23322378

@@ -2342,6 +2388,16 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has
23422388
goto exit;
23432389
}
23442390

2391+
if (UNEXPECTED(zobj->ce->__isset)) {
2392+
uint32_t *guard = zend_get_property_guard(zobj, name);
2393+
if (!((*guard) & IN_ISSET)) {
2394+
(*guard) |= IN_ISSET;
2395+
result = zend_std_has_property(zobj, name, has_set_exists, cache_slot);
2396+
(*guard) &= ~IN_ISSET;
2397+
return result;
2398+
}
2399+
}
2400+
23452401
return zend_std_has_property(zobj, name, has_set_exists, cache_slot);
23462402
}
23472403
}

0 commit comments

Comments
 (0)
Please sign in to comment.