Skip to content

Commit 376e90f

Browse files
committed
Fix circumvented added hooks in JIT
The following code poses a problem in the JIT: ```php class A { public $prop = 1; } class B extends A { public $prop = 1 { get => parent::$prop::get() * 2; } } function test(A $a) { var_dump($a->prop); } test(new B); ``` The JIT would assume A::$prop in test() could be accessed directly through OBJ_PROP_NUM(). However, since child classes can add new hooks to existing properties, this assumption no longer holds. To avoid introducing more JIT checks, a hooked property that overrides a unhooked property now results in a separate zval slot that is used instead of the parent slot. This causes the JIT to pick the slow path due to an IS_UNDEF value in the parent slot. zend_class_entry.properties_info_table poses a problem in that zend_get_property_info_for_slot() and friends will be called using the child slot, which does not store its property info, since the parent slot already does. In this case, zend_get_property_info_for_slot() now provides a fallback that will iterate all property infos to find the correct one. This also uncovered a bug (see Zend/tests/property_hooks/dump.phpt) where the default value of a parent property would accidentally be inherited by the child property. Fixes GH-17376 Closes GH-17870
1 parent e0c69dd commit 376e90f

File tree

7 files changed

+184
-12
lines changed

7 files changed

+184
-12
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ PHP NEWS
77
(ilutov)
88
. Fixed accidentally inherited default value in overridden virtual properties.
99
(ilutov)
10+
. Fixed bug GH-17376 (Broken JIT polymorphism for property hooks added to
11+
child class). (ilutov)
1012

1113
27 Feb 2025, PHP 8.4.5
1214

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
--TEST--
2+
GH-17376: Child classes may add hooks to plain properties
3+
--INI--
4+
# Avoid triggering for main, trigger for test so we get a side-trace for property hooks
5+
opcache.jit_hot_func=2
6+
--FILE--
7+
<?php
8+
9+
class A {
10+
public $prop = 1;
11+
}
12+
13+
class B extends A {
14+
public $prop = 1 {
15+
get {
16+
echo __METHOD__, "\n";
17+
return $this->prop;
18+
}
19+
set {
20+
echo __METHOD__, "\n";
21+
$this->prop = $value;
22+
}
23+
}
24+
}
25+
26+
function test(A $a) {
27+
echo "read\n";
28+
var_dump($a->prop);
29+
echo "write\n";
30+
$a->prop = 42;
31+
echo "read-write\n";
32+
$a->prop += 43;
33+
echo "pre-inc\n";
34+
++$a->prop;
35+
echo "pre-dec\n";
36+
--$a->prop;
37+
echo "post-inc\n";
38+
$a->prop++;
39+
echo "post-dec\n";
40+
$a->prop--;
41+
}
42+
43+
echo "A\n";
44+
test(new A);
45+
46+
echo "\nA\n";
47+
test(new A);
48+
49+
echo "\nB\n";
50+
test(new B);
51+
52+
echo "\nB\n";
53+
test(new B);
54+
55+
?>
56+
--EXPECT--
57+
A
58+
read
59+
int(1)
60+
write
61+
read-write
62+
pre-inc
63+
pre-dec
64+
post-inc
65+
post-dec
66+
67+
A
68+
read
69+
int(1)
70+
write
71+
read-write
72+
pre-inc
73+
pre-dec
74+
post-inc
75+
post-dec
76+
77+
B
78+
read
79+
B::$prop::get
80+
int(1)
81+
write
82+
B::$prop::set
83+
read-write
84+
B::$prop::get
85+
B::$prop::set
86+
pre-inc
87+
B::$prop::get
88+
B::$prop::set
89+
pre-dec
90+
B::$prop::get
91+
B::$prop::set
92+
post-inc
93+
B::$prop::get
94+
B::$prop::set
95+
post-dec
96+
B::$prop::get
97+
B::$prop::set
98+
99+
B
100+
read
101+
B::$prop::get
102+
int(1)
103+
write
104+
B::$prop::set
105+
read-write
106+
B::$prop::get
107+
B::$prop::set
108+
pre-inc
109+
B::$prop::get
110+
B::$prop::set
111+
pre-dec
112+
B::$prop::get
113+
B::$prop::set
114+
post-inc
115+
B::$prop::get
116+
B::$prop::set
117+
post-dec
118+
B::$prop::get
119+
B::$prop::set

Zend/zend_compile.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ typedef struct _zend_property_info {
463463
#define OBJ_PROP_TO_OFFSET(num) \
464464
((uint32_t)(XtOffsetOf(zend_object, properties_table) + sizeof(zval) * (num)))
465465
#define OBJ_PROP_TO_NUM(offset) \
466-
((offset - OBJ_PROP_TO_OFFSET(0)) / sizeof(zval))
466+
(((offset) - OBJ_PROP_TO_OFFSET(0)) / sizeof(zval))
467467

468468
typedef struct _zend_class_constant {
469469
zval value; /* flags are stored in u2 */

Zend/zend_inheritance.c

+33-8
Original file line numberDiff line numberDiff line change
@@ -1478,14 +1478,32 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke
14781478
zend_error_noreturn(E_COMPILE_ERROR, "Access level to %s::$%s must be %s (as in class %s)%s", ZSTR_VAL(ce->name), ZSTR_VAL(key), zend_visibility_string(parent_info->flags), ZSTR_VAL(parent_info->ce->name), (parent_info->flags&ZEND_ACC_PUBLIC) ? "" : " or weaker");
14791479
}
14801480
if (!(child_info->flags & ZEND_ACC_STATIC) && !(parent_info->flags & ZEND_ACC_VIRTUAL)) {
1481+
/* If we added hooks to the child property, we use the child's slot for
1482+
* storage to keep the parent slot set to IS_UNDEF. This automatically
1483+
* picks the slow path in the JIT. */
1484+
bool use_child_prop = !parent_info->hooks && child_info->hooks;
1485+
1486+
if (use_child_prop && child_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET) {
1487+
child_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count);
1488+
ce->default_properties_count++;
1489+
ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS);
1490+
zval *property_default_ptr = &ce->default_properties_table[OBJ_PROP_TO_NUM(child_info->offset)];
1491+
ZVAL_UNDEF(property_default_ptr);
1492+
Z_PROP_FLAG_P(property_default_ptr) = IS_PROP_UNINIT;
1493+
}
1494+
14811495
int parent_num = OBJ_PROP_TO_NUM(parent_info->offset);
14821496
if (child_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) {
1483-
int child_num = OBJ_PROP_TO_NUM(child_info->offset);
1484-
14851497
/* Don't keep default properties in GC (they may be freed by opcache) */
14861498
zval_ptr_dtor_nogc(&(ce->default_properties_table[parent_num]));
1487-
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num];
1488-
ZVAL_UNDEF(&ce->default_properties_table[child_num]);
1499+
1500+
if (use_child_prop) {
1501+
ZVAL_UNDEF(&ce->default_properties_table[parent_num]);
1502+
} else {
1503+
int child_num = OBJ_PROP_TO_NUM(child_info->offset);
1504+
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num];
1505+
ZVAL_UNDEF(&ce->default_properties_table[child_num]);
1506+
}
14891507
} else {
14901508
/* Default value was removed in child, remove it from parent too. */
14911509
if (ZEND_TYPE_IS_SET(child_info->type)) {
@@ -1495,7 +1513,9 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke
14951513
}
14961514
}
14971515

1498-
child_info->offset = parent_info->offset;
1516+
if (!use_child_prop) {
1517+
child_info->offset = parent_info->offset;
1518+
}
14991519
child_info->flags &= ~ZEND_ACC_VIRTUAL;
15001520
}
15011521

@@ -1670,7 +1690,8 @@ void zend_build_properties_info_table(zend_class_entry *ce)
16701690
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) {
16711691
if (prop->ce == ce && (prop->flags & ZEND_ACC_STATIC) == 0
16721692
&& !(prop->flags & ZEND_ACC_VIRTUAL)) {
1673-
table[OBJ_PROP_TO_NUM(prop->offset)] = prop;
1693+
uint32_t prop_table_offset = OBJ_PROP_TO_NUM(!(prop->prototype->flags & ZEND_ACC_VIRTUAL) ? prop->prototype->offset : prop->offset);
1694+
table[prop_table_offset] = prop;
16741695
}
16751696
} ZEND_HASH_FOREACH_END();
16761697
}
@@ -1684,8 +1705,12 @@ ZEND_API void zend_verify_hooked_property(zend_class_entry *ce, zend_property_in
16841705
/* We specified a default value (otherwise offset would be -1), but the virtual flag wasn't
16851706
* removed during inheritance. */
16861707
if ((prop_info->flags & ZEND_ACC_VIRTUAL) && prop_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) {
1687-
zend_error_noreturn(E_COMPILE_ERROR,
1688-
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name));
1708+
if (Z_TYPE(ce->default_properties_table[OBJ_PROP_TO_NUM(prop_info->offset)]) == IS_UNDEF) {
1709+
prop_info->offset = ZEND_VIRTUAL_PROPERTY_OFFSET;
1710+
} else {
1711+
zend_error_noreturn(E_COMPILE_ERROR,
1712+
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name));
1713+
}
16891714
}
16901715
/* If the property turns backed during inheritance and no type and default value are set, we want
16911716
* the default value to be null. */

Zend/zend_lazy_objects.c

+5-1
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,11 @@ zend_property_info *zend_lazy_object_get_property_info_for_slot(zend_object *obj
806806
zend_property_info **table = obj->ce->properties_info_table;
807807
intptr_t prop_num = slot - obj->properties_table;
808808
if (prop_num >= 0 && prop_num < obj->ce->default_properties_count) {
809-
return table[prop_num];
809+
if (table[prop_num]) {
810+
return table[prop_num];
811+
} else {
812+
return zend_get_property_info_for_slot_slow(obj, slot);
813+
}
810814
}
811815

812816
if (!zend_lazy_object_initialized(obj)) {

Zend/zend_objects_API.c

+12
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,15 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_del(zend_object *object) /* {{{ *
200200
}
201201
}
202202
/* }}} */
203+
204+
ZEND_API ZEND_COLD zend_property_info *zend_get_property_info_for_slot_slow(zend_object *obj, zval *slot)
205+
{
206+
uintptr_t offset = (uintptr_t)slot - (uintptr_t)obj->properties_table;
207+
zend_property_info *prop_info;
208+
ZEND_HASH_MAP_FOREACH_PTR(&obj->ce->properties_info, prop_info) {
209+
if (prop_info->offset == offset) {
210+
return prop_info;
211+
}
212+
} ZEND_HASH_FOREACH_END();
213+
return NULL;
214+
}

Zend/zend_objects_API.h

+12-2
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,20 @@ static zend_always_inline void *zend_object_alloc(size_t obj_size, zend_class_en
9696
return obj;
9797
}
9898

99+
ZEND_API ZEND_COLD zend_property_info *zend_get_property_info_for_slot_slow(zend_object *obj, zval *slot);
100+
99101
/* Use when 'slot' was obtained directly from obj->properties_table, or when
100102
* 'obj' can not be lazy. Otherwise, use zend_get_property_info_for_slot(). */
101103
static inline zend_property_info *zend_get_property_info_for_slot_self(zend_object *obj, zval *slot)
102104
{
103105
zend_property_info **table = obj->ce->properties_info_table;
104106
intptr_t prop_num = slot - obj->properties_table;
105107
ZEND_ASSERT(prop_num >= 0 && prop_num < obj->ce->default_properties_count);
106-
return table[prop_num];
108+
if (table[prop_num]) {
109+
return table[prop_num];
110+
} else {
111+
return zend_get_property_info_for_slot_slow(obj, slot);
112+
}
107113
}
108114

109115
static inline zend_property_info *zend_get_property_info_for_slot(zend_object *obj, zval *slot)
@@ -114,7 +120,11 @@ static inline zend_property_info *zend_get_property_info_for_slot(zend_object *o
114120
zend_property_info **table = obj->ce->properties_info_table;
115121
intptr_t prop_num = slot - obj->properties_table;
116122
ZEND_ASSERT(prop_num >= 0 && prop_num < obj->ce->default_properties_count);
117-
return table[prop_num];
123+
if (table[prop_num]) {
124+
return table[prop_num];
125+
} else {
126+
return zend_get_property_info_for_slot_slow(obj, slot);
127+
}
118128
}
119129

120130
/* Helper for cases where we're only interested in property info of typed properties. */

0 commit comments

Comments
 (0)