-
Notifications
You must be signed in to change notification settings - Fork 7.8k
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
Fix circumvented added hooks in JIT #17870
Conversation
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 phpGH-17376
4cf0f0e
to
add3e2b
Compare
I have no clue what this Windows failure is about. 😞 I can't reproduce on Linux with asan/msan/valgrind, w/wo opcache, nts/zts, etc. @cmb69 Can you help? I don't have a VM... |
It's a JIT issue. Running ext\random\tests\02_engine\all_serialize_user.phpt under ASan/tracing JIT:
Does that help? |
Thank you very much! It might, I'll have a look very soon. |
Z_PROP_TABLE_OFFSET already gives us an index into a void* array. Thus, we only need to multiply by sizeof(void*).
Looks good. :) |
@cmb69 Indeed, it did help 😊 Thanks again! |
21ec227
to
060a409
Compare
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.
This looks good to me!
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.
I didn't understand the implementation in all details yet, but the idea is clear and seems should work.
In case child hook updates the real property, only the child real property is changed, and the parent property is kept UNDEF forever. Right?
Please, answer my questions. I'll review this once again tomorrow.
@nielsdos please also take a look.
public $prop { | ||
public $prop = 42 { |
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.
return table[prop_num]; | ||
if (table[prop_num]) { |
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.
When table[prop_num]
may be NULL?
Only for regular properties overridden with hooked ones?
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.
Only for regular properties overridden with hooked ones?
They will only ever be NULL
for properties with hooks that override properties without hooks. This could add an EXPECTED()
guard, if you like.
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.
OK. I don't care.
Yes, exactly. |
ZEND_HASH_MAP_FOREACH_PTR(&obj->ce->properties_info, prop_info) { | ||
if (prop_info->offset == offset) { | ||
return prop_info; | ||
} | ||
} ZEND_HASH_FOREACH_END(); |
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.
Think, if it's possible to improve this linear search.
This is not a big problem or stopper, but it would be good o avoid this.
May be we may store the prop_info somehow differently...
Or may be check only the child properties (skipping ce->parrent->default_properties_count
)...
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.
May be we may store the prop_info somehow differently...
A straight-forward solution in that regard would be to store a clone of properties_info_table
that sets both the parent and child offset to the child property. This was not done in properties_info_table
to avoid breaking iteration over the table. That would not be ABI compatible though (modification of zend_class_entry
), so should likely be done in master
only.
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.
Checking only child properties will already be a welcome improvement
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.
The concept seems right, but this may break some assumptions elsewhere in code about slots/inheritance.
For example, this code:
<?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);
Now outputs:
array(2) {
["prop"]=>
int(3)
["prop"]=>
int(3)
}
Which is wrong, this worked prior to this patch.
I also tried to play a bit with multiple parent classes, to test how that interacts. But I got some weird behaviour that I cannot understand. This may be another bug unrelated to this but I'll bring it up here to ask and show what I tried to do. This breaks in PHP with or without JIT, it causes an infinite recursion:
<?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($a->prop);
}
test(new C);
Something seems off here or I just don't understand how this is supposed to work.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree ofc. That said, this excerpt comes from zend_declare_typed_property()
which will trigger much more commonly than this code path.
ZEND_HASH_MAP_FOREACH_PTR(&obj->ce->properties_info, prop_info) { | ||
if (prop_info->offset == offset) { | ||
return prop_info; | ||
} | ||
} ZEND_HASH_FOREACH_END(); |
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.
Checking only child properties will already be a welcome improvement
Nice find, thank you! I'll have a look. |
The property info prototype would point to the parent instead of the first ancestor.
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.
I found more related problems...
<?php
class A {
public $prop = 1;
}
class B extends A {
public $prop = 2 { get => parent::$prop::get() * 2; }
}
class C extends B {
public $prop = 3;
}
function test(A $a) {
var_dump($a);
var_dump((array)$a);
var_dump(get_object_vars($a));
var_dump(unserialize(serialize($a)));
var_dump(json_decode(json_encode($a)));
}
test(new C);
now output
object(C)#1 (1) {
["prop"]=>
int(3)
}
array(1) {
["prop"]=>
int(3)
}
array(1) {
["prop"]=>
int(6)
}
object(C)#2 (1) {
["prop"]=>
int(3)
}
object(stdClass)#2 (1) {
["prop"]=>
int(6)
}
Interesting this breaks ext/json
and not serialize()
. This may also affect other third-party extensions.
Also try to pass new B
instead of new C
. I don't know what should it return, but the output is definitely inconsistent (sometimes 2
sometimes 4
). Way be I'm wrong here.
Please also check for consistency of indirect calls to set
hook from unserialize()
and other similar places
@dstogov This looks correct at first glance. Note that hooks are inherited in child properties, so while |
OK. If this is on purpose it's hard to understand the purpose. |
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.
I wanted to play a bit with this and lazy objects, but was blocked by another issue (unrelated to this). (#17941)
I think this is fine now.
Thank you all for the reviews and testing! |
The following code poses a problem in the JIT:
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