-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Fix circumvented added hooks in JIT #17870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
add3e2b
7042ec6
a48f879
060a409
8ca7bc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
--TEST-- | ||
GH-17376: Child classes may add hooks to plain properties | ||
--INI-- | ||
# Avoid triggering for main, trigger for test so we get a side-trace for property hooks | ||
opcache.jit_hot_func=2 | ||
--FILE-- | ||
<?php | ||
|
||
class A { | ||
public $prop = 1; | ||
} | ||
|
||
class B extends A { | ||
public $prop = 1 { | ||
get { | ||
echo __METHOD__, "\n"; | ||
return $this->prop; | ||
} | ||
set { | ||
echo __METHOD__, "\n"; | ||
$this->prop = $value; | ||
} | ||
} | ||
} | ||
|
||
function test(A $a) { | ||
echo "read\n"; | ||
var_dump($a->prop); | ||
echo "write\n"; | ||
$a->prop = 42; | ||
echo "read-write\n"; | ||
$a->prop += 43; | ||
echo "pre-inc\n"; | ||
++$a->prop; | ||
echo "pre-dec\n"; | ||
--$a->prop; | ||
echo "post-inc\n"; | ||
$a->prop++; | ||
echo "post-dec\n"; | ||
$a->prop--; | ||
} | ||
|
||
echo "A\n"; | ||
test(new A); | ||
|
||
echo "\nA\n"; | ||
test(new A); | ||
|
||
echo "\nB\n"; | ||
test(new B); | ||
|
||
echo "\nB\n"; | ||
test(new B); | ||
|
||
?> | ||
--EXPECT-- | ||
A | ||
read | ||
int(1) | ||
write | ||
read-write | ||
pre-inc | ||
pre-dec | ||
post-inc | ||
post-dec | ||
|
||
A | ||
read | ||
int(1) | ||
write | ||
read-write | ||
pre-inc | ||
pre-dec | ||
post-inc | ||
post-dec | ||
|
||
B | ||
read | ||
B::$prop::get | ||
int(1) | ||
write | ||
B::$prop::set | ||
read-write | ||
B::$prop::get | ||
B::$prop::set | ||
pre-inc | ||
B::$prop::get | ||
B::$prop::set | ||
pre-dec | ||
B::$prop::get | ||
B::$prop::set | ||
post-inc | ||
B::$prop::get | ||
B::$prop::set | ||
post-dec | ||
B::$prop::get | ||
B::$prop::set | ||
|
||
B | ||
read | ||
B::$prop::get | ||
int(1) | ||
write | ||
B::$prop::set | ||
read-write | ||
B::$prop::get | ||
B::$prop::set | ||
pre-inc | ||
B::$prop::get | ||
B::$prop::set | ||
pre-dec | ||
B::$prop::get | ||
B::$prop::set | ||
post-inc | ||
B::$prop::get | ||
B::$prop::set | ||
post-dec | ||
B::$prop::get | ||
B::$prop::set |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--TEST-- | ||
GH-17376: Mutli-level inheritance with property hooks | ||
--FILE-- | ||
<?php | ||
|
||
class A { | ||
public $prop = 1; | ||
} | ||
|
||
class B extends A { | ||
public $prop = 1 { get => parent::$prop::get() * 2; } | ||
} | ||
|
||
class C extends B { | ||
public $prop = 3; | ||
} | ||
|
||
function test(A $a) { | ||
var_dump((array)$a); | ||
} | ||
|
||
test(new C); | ||
|
||
?> | ||
--EXPECT-- | ||
array(1) { | ||
["prop"]=> | ||
int(3) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ class P { | |
} | ||
|
||
class C extends P { | ||
public $prop { | ||
public $prop = 42 { | ||
get => parent::$prop::get(); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1436,7 +1436,7 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke | |
} | ||
if (!(parent_info->flags & ZEND_ACC_PRIVATE)) { | ||
if (!(parent_info->ce->ce_flags & ZEND_ACC_INTERFACE)) { | ||
child_info->prototype = parent_info; | ||
child_info->prototype = parent_info->prototype; | ||
} | ||
|
||
if (UNEXPECTED((parent_info->flags & ZEND_ACC_STATIC) != (child_info->flags & ZEND_ACC_STATIC))) { | ||
|
@@ -1478,17 +1478,38 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke | |
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"); | ||
} | ||
if (!(child_info->flags & ZEND_ACC_STATIC) && !(parent_info->flags & ZEND_ACC_VIRTUAL)) { | ||
/* If we added hooks to the child property, we use the child's slot for | ||
* storage to keep the parent slot set to IS_UNDEF. This automatically | ||
* picks the slow path in the JIT. */ | ||
bool use_child_prop = !parent_info->hooks && child_info->hooks; | ||
|
||
if (use_child_prop && child_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET) { | ||
child_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count); | ||
ce->default_properties_count++; | ||
ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing reallocs with linear size increases is not very efficient, but okay this should be rare and happens during inheritance so it shouldn't be a big problem / showstopper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fully agree ofc. That said, this excerpt comes from |
||
zval *property_default_ptr = &ce->default_properties_table[OBJ_PROP_TO_NUM(child_info->offset)]; | ||
ZVAL_UNDEF(property_default_ptr); | ||
Z_PROP_FLAG_P(property_default_ptr) = IS_PROP_UNINIT; | ||
} | ||
|
||
if (child_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) { | ||
int parent_num = OBJ_PROP_TO_NUM(parent_info->offset); | ||
int child_num = OBJ_PROP_TO_NUM(child_info->offset); | ||
|
||
/* Don't keep default properties in GC (they may be freed by opcache) */ | ||
zval_ptr_dtor_nogc(&(ce->default_properties_table[parent_num])); | ||
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num]; | ||
ZVAL_UNDEF(&ce->default_properties_table[child_num]); | ||
|
||
if (use_child_prop) { | ||
ZVAL_UNDEF(&ce->default_properties_table[parent_num]); | ||
} else { | ||
int child_num = OBJ_PROP_TO_NUM(child_info->offset); | ||
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num]; | ||
ZVAL_UNDEF(&ce->default_properties_table[child_num]); | ||
} | ||
} | ||
|
||
child_info->offset = parent_info->offset; | ||
if (!use_child_prop) { | ||
child_info->offset = parent_info->offset; | ||
} | ||
child_info->flags &= ~ZEND_ACC_VIRTUAL; | ||
} | ||
|
||
|
@@ -1663,7 +1684,8 @@ void zend_build_properties_info_table(zend_class_entry *ce) | |
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { | ||
if (prop->ce == ce && (prop->flags & ZEND_ACC_STATIC) == 0 | ||
&& !(prop->flags & ZEND_ACC_VIRTUAL)) { | ||
table[OBJ_PROP_TO_NUM(prop->offset)] = prop; | ||
uint32_t prop_table_offset = OBJ_PROP_TO_NUM(!(prop->prototype->flags & ZEND_ACC_VIRTUAL) ? prop->prototype->offset : prop->offset); | ||
table[prop_table_offset] = prop; | ||
} | ||
} ZEND_HASH_FOREACH_END(); | ||
} | ||
|
@@ -1677,8 +1699,12 @@ ZEND_API void zend_verify_hooked_property(zend_class_entry *ce, zend_property_in | |
/* We specified a default value (otherwise offset would be -1), but the virtual flag wasn't | ||
* removed during inheritance. */ | ||
if ((prop_info->flags & ZEND_ACC_VIRTUAL) && prop_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET) { | ||
zend_error_noreturn(E_COMPILE_ERROR, | ||
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name)); | ||
if (Z_TYPE(ce->default_properties_table[OBJ_PROP_TO_NUM(prop_info->offset)]) == IS_UNDEF) { | ||
prop_info->offset = ZEND_VIRTUAL_PROPERTY_OFFSET; | ||
} else { | ||
zend_error_noreturn(E_COMPILE_ERROR, | ||
"Cannot specify default value for virtual hooked property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(prop_name)); | ||
} | ||
} | ||
/* If the property turns backed during inheritance and no type and default value are set, we want | ||
* the default value to be null. */ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -806,7 +806,11 @@ zend_property_info *zend_lazy_object_get_property_info_for_slot(zend_object *obj | |
zend_property_info **table = obj->ce->properties_info_table; | ||
intptr_t prop_num = slot - obj->properties_table; | ||
if (prop_num >= 0 && prop_num < obj->ce->default_properties_count) { | ||
return table[prop_num]; | ||
if (table[prop_num]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
They will only ever be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. I don't care. |
||
return table[prop_num]; | ||
} else { | ||
return zend_get_property_info_for_slot_slow(obj, slot); | ||
} | ||
} | ||
|
||
if (!zend_lazy_object_initialized(obj)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -200,3 +200,15 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_del(zend_object *object) /* {{{ * | |
} | ||
} | ||
/* }}} */ | ||
|
||
ZEND_API ZEND_COLD zend_property_info *zend_get_property_info_for_slot_slow(zend_object *obj, zval *slot) | ||
{ | ||
uintptr_t offset = (uintptr_t)slot - (uintptr_t)obj->properties_table; | ||
zend_property_info *prop_info; | ||
ZEND_HASH_MAP_FOREACH_PTR(&obj->ce->properties_info, prop_info) { | ||
if (prop_info->offset == offset) { | ||
return prop_info; | ||
} | ||
} ZEND_HASH_FOREACH_END(); | ||
Comment on lines
+208
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think, if it's possible to improve this linear search. May be we may store the prop_info somehow differently... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A straight-forward solution in that regard would be to store a clone of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking only child properties will already be a welcome improvement |
||
return NULL; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need these tests changes?
Does the patch break some previous behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this patch also fixes a bug. Properties do not inherit default values, but we accidentally did for properties with hooks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the fix may be done separately, it's better to do this separately (add bug report and so on).
If the fix relays on new inheritance logic, and can't be simple separated, don't do this.