Skip to content

Commit add3e2b

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
1 parent 1eb6751 commit add3e2b

10 files changed

+195
-20
lines changed

Zend/tests/property_hooks/dump.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Test {
3434
}
3535

3636
class Child extends Test {
37-
public $addedHooks {
37+
public $addedHooks = 'addedHooks' {
3838
get { return strtoupper(parent::$addedHooks::get()); }
3939
}
4040
private $changed = 'changed Child' {

Zend/tests/property_hooks/generator_hook_002.phpt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class A {
1010
class B extends A {
1111
public $prop {
1212
get {
13+
yield parent::$prop::get();
1314
yield parent::$prop::get() + 1;
1415
yield parent::$prop::get() + 2;
1516
yield parent::$prop::get() + 3;
@@ -24,6 +25,7 @@ foreach ($b->prop as $value) {
2425

2526
?>
2627
--EXPECT--
27-
int(43)
28-
int(44)
29-
int(45)
28+
NULL
29+
int(1)
30+
int(2)
31+
int(3)
Lines changed: 119 additions & 0 deletions
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/tests/property_hooks/parent_get_plain.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class P {
88
}
99

1010
class C extends P {
11-
public $prop {
11+
public $prop = 42 {
1212
get => parent::$prop::get();
1313
}
1414
}

Zend/zend_compile.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,10 @@ 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))
467+
468+
#define Z_PROP_TABLE_OFFSET(prop_info) \
469+
OBJ_PROP_TO_NUM(!((prop_info)->prototype->flags & ZEND_ACC_VIRTUAL) ? (prop_info)->prototype->offset : (prop_info)->offset)
467470

468471
typedef struct _zend_class_constant {
469472
zval value; /* flags are stored in u2 */

Zend/zend_inheritance.c

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,17 +1478,38 @@ 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 props to the child property, we use the childs slot for
1482+
* storage to keep the parent slot set to null. This automatically picks
1483+
* 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
if (child_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) {
14821496
int parent_num = OBJ_PROP_TO_NUM(parent_info->offset);
1483-
int child_num = OBJ_PROP_TO_NUM(child_info->offset);
14841497

14851498
/* Don't keep default properties in GC (they may be freed by opcache) */
14861499
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]);
1500+
1501+
if (use_child_prop) {
1502+
ZVAL_UNDEF(&ce->default_properties_table[parent_num]);
1503+
} else {
1504+
int child_num = OBJ_PROP_TO_NUM(child_info->offset);
1505+
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num];
1506+
ZVAL_UNDEF(&ce->default_properties_table[child_num]);
1507+
}
14891508
}
14901509

1491-
child_info->offset = parent_info->offset;
1510+
if (!use_child_prop) {
1511+
child_info->offset = parent_info->offset;
1512+
}
14921513
child_info->flags &= ~ZEND_ACC_VIRTUAL;
14931514
}
14941515

@@ -1663,7 +1684,7 @@ void zend_build_properties_info_table(zend_class_entry *ce)
16631684
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) {
16641685
if (prop->ce == ce && (prop->flags & ZEND_ACC_STATIC) == 0
16651686
&& !(prop->flags & ZEND_ACC_VIRTUAL)) {
1666-
table[OBJ_PROP_TO_NUM(prop->offset)] = prop;
1687+
table[Z_PROP_TABLE_OFFSET(prop)] = prop;
16671688
}
16681689
} ZEND_HASH_FOREACH_END();
16691690
}
@@ -1677,8 +1698,12 @@ ZEND_API void zend_verify_hooked_property(zend_class_entry *ce, zend_property_in
16771698
/* We specified a default value (otherwise offset would be -1), but the virtual flag wasn't
16781699
* removed during inheritance. */
16791700
if ((prop_info->flags & ZEND_ACC_VIRTUAL) && prop_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) {
1680-
zend_error_noreturn(E_COMPILE_ERROR,
1681-
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name));
1701+
if (Z_TYPE(ce->default_properties_table[OBJ_PROP_TO_NUM(prop_info->offset)]) == IS_UNDEF) {
1702+
prop_info->offset = ZEND_VIRTUAL_PROPERTY_OFFSET;
1703+
} else {
1704+
zend_error_noreturn(E_COMPILE_ERROR,
1705+
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name));
1706+
}
16821707
}
16831708
/* If the property turns backed during inheritance and no type and default value are set, we want
16841709
* the default value to be null. */

Zend/zend_lazy_objects.c

Lines changed: 5 additions & 1 deletion
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 12 additions & 2 deletions
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. */

ext/opcache/jit/zend_jit_ir.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14377,7 +14377,7 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
1437714377
ref = ir_CONST_ADDR(prop_info);
1437814378
} else {
1437914379
int prop_info_offset =
14380-
(((prop_info->offset - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
14380+
(((Z_PROP_TABLE_OFFSET(prop_info) - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
1438114381

1438214382
ref = ir_LOAD_A(ir_ADD_OFFSET(obj_ref, offsetof(zend_object, ce)));
1438314383
ref = ir_LOAD_A(ir_ADD_OFFSET(ref, offsetof(zend_class_entry, properties_info_table)));
@@ -14778,7 +14778,7 @@ static int zend_jit_assign_obj(zend_jit_ctx *jit,
1477814778
ref = ir_CONST_ADDR(prop_info);
1477914779
} else {
1478014780
int prop_info_offset =
14781-
(((prop_info->offset - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
14781+
(((Z_PROP_TABLE_OFFSET(prop_info) - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
1478214782

1478314783
ref = ir_LOAD_A(ir_ADD_OFFSET(obj_ref, offsetof(zend_object, ce)));
1478414784
ref = ir_LOAD_A(ir_ADD_OFFSET(ref, offsetof(zend_class_entry, properties_info_table)));
@@ -15134,7 +15134,7 @@ static int zend_jit_assign_obj_op(zend_jit_ctx *jit,
1513415134
ref = ir_CONST_ADDR(prop_info);
1513515135
} else {
1513615136
int prop_info_offset =
15137-
(((prop_info->offset - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
15137+
(((Z_PROP_TABLE_OFFSET(prop_info) - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
1513815138

1513915139
ref = ir_LOAD_A(ir_ADD_OFFSET(obj_ref, offsetof(zend_object, ce)));
1514015140
ref = ir_LOAD_A(ir_ADD_OFFSET(ref, offsetof(zend_class_entry, properties_info_table)));
@@ -15524,7 +15524,7 @@ static int zend_jit_incdec_obj(zend_jit_ctx *jit,
1552415524
ref = ir_CONST_ADDR(prop_info);
1552515525
} else {
1552615526
int prop_info_offset =
15527-
(((prop_info->offset - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
15527+
(((Z_PROP_TABLE_OFFSET(prop_info) - (sizeof(zend_object) - sizeof(zval))) / sizeof(zval)) * sizeof(void*));
1552815528

1552915529
ref = ir_LOAD_A(ir_ADD_OFFSET(obj_ref, offsetof(zend_object, ce)));
1553015530
ref = ir_LOAD_A(ir_ADD_OFFSET(ref, offsetof(zend_class_entry, properties_info_table)));

0 commit comments

Comments
 (0)