-
Notifications
You must be signed in to change notification settings - Fork 8k
Make guards context-dependent #14994
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
base: master
Are you sure you want to change the base?
Changes from all commits
95d28fd
1bc65f2
ade1e85
f5adc6c
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,42 @@ | ||
| --TEST-- | ||
| GH-14983: Shutdown disregards guards active on bailout | ||
| --SKIPIF-- | ||
| <?php if (getenv("SKIP_SLOW_TESTS")) die('skip slow test'); ?> | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| ini_set('max_execution_time', 1); | ||
| $loop = true; | ||
|
|
||
| class A { | ||
| function __get($name) { | ||
| global $loop; | ||
| if ($loop) { | ||
| while (true) {} | ||
| } | ||
| echo __METHOD__, "\n"; | ||
| return $this->{$name}; | ||
| } | ||
| } | ||
|
|
||
| global $a; | ||
| $a = new A; | ||
|
|
||
| function shutdown() { | ||
| global $a; | ||
| global $loop; | ||
| $loop = false; | ||
| var_dump($a->foo); | ||
| } | ||
|
|
||
| register_shutdown_function('shutdown'); | ||
|
|
||
| $a->foo; | ||
|
|
||
| ?> | ||
| --EXPECTF-- | ||
| Fatal error: Maximum execution time of 1 second exceeded in %s on line %d | ||
| A::__get | ||
|
|
||
| Warning: Undefined property: A::$foo in %s on line %d | ||
| NULL |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| --TEST-- | ||
| GH-14983: Guards from fibers are separated from guards from main context | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| class Foo { | ||
| public function __get($prop) { | ||
| echo "__set($prop)\n"; | ||
| if (Fiber::getCurrent()) { | ||
| Fiber::suspend(); | ||
| echo "Resuming\n"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $foo = new Foo(); | ||
|
|
||
| $fiber = new Fiber(function () use ($foo) { | ||
| $foo->bar; | ||
| }); | ||
|
|
||
| $value = $fiber->start(); | ||
| echo "Suspended\n"; | ||
| $foo->bar; | ||
| $fiber->resume('test'); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| __set(bar) | ||
| Suspended | ||
| __set(bar) | ||
| Resuming |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -565,18 +565,27 @@ ZEND_API uint32_t *zend_get_property_guard(zend_object *zobj, zend_string *membe | |
| zval *zv; | ||
| uint32_t *ptr; | ||
|
|
||
| if (EXPECTED(EG(guard_context) == 0)) { | ||
| member = zend_string_copy(member); | ||
| } else { | ||
| member = zend_string_init(ZSTR_VAL(member), ZSTR_LEN(member), false); | ||
| ZSTR_H(member) = zend_string_hash_val(member) + EG(guard_context); | ||
|
Comment on lines
+571
to
+572
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 don't particularly like having the overhead of a string allocation for every single property hook access in a fiber. It will also fail the trivial str == member comparison and require a full comparison for any access in fiber context. It's probably okay for just magic __get/__set, but I expect property hooks to be quite common. Can you just allocate 8 bytes more for zend_objects using guards? And store the guard_context there - at least for the case where no nested (or parallel in case of fibers) access happens? The hack with the hash is fine in case it's actually going to the hashtable. But too much overhead for the common scenario. 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.
Hooks don't do guards anymore, so. :) 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. Oh, right. I still think allocating a few more bytes for the guard wouldn't do harm though, i.e. common case. 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. This not only requires a new string allocation for each __get/__set call in a fiber. These strings are also going to be kept in guards hash until the object destruction. 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. That's true. I can try @bwoebi's approach, but that also means increasing the allocation size of all objects with guards, even when they don't use fibers. I would assume that applications using fibers are generally more modern, possibly also making less use of |
||
| } | ||
|
|
||
| ZEND_ASSERT(zobj->ce->ce_flags & ZEND_ACC_USE_GUARDS); | ||
| zv = zend_get_guard_value(zobj); | ||
| if (EXPECTED(Z_TYPE_P(zv) == IS_STRING)) { | ||
| zend_string *str = Z_STR_P(zv); | ||
| if (EXPECTED(str == member) || | ||
| /* str and member don't necessarily have a pre-calculated hash value here */ | ||
| EXPECTED(zend_string_equal_content(str, member))) { | ||
| (EXPECTED(ZSTR_HASH(str) == ZSTR_HASH(member) | ||
| && zend_string_equal_content(str, member)))) { | ||
| zend_string_release(member); | ||
| return &Z_GUARD_P(zv); | ||
| } else if (EXPECTED(Z_GUARD_P(zv) == 0)) { | ||
| zval_ptr_dtor_str(zv); | ||
| ZVAL_STR_COPY(zv, member); | ||
| /* Transfer ownership. */ | ||
| ZVAL_STR(zv, member); | ||
| return &Z_GUARD_P(zv); | ||
| } else { | ||
| ALLOC_HASHTABLE(guards); | ||
|
|
@@ -592,18 +601,22 @@ ZEND_API uint32_t *zend_get_property_guard(zend_object *zobj, zend_string *membe | |
| ZEND_ASSERT(guards != NULL); | ||
| zv = zend_hash_find(guards, member); | ||
| if (zv != NULL) { | ||
| zend_string_release(member); | ||
| return (uint32_t*)(((uintptr_t)Z_PTR_P(zv)) & ~1); | ||
| } | ||
| } else { | ||
| ZEND_ASSERT(Z_TYPE_P(zv) == IS_UNDEF); | ||
| ZVAL_STR_COPY(zv, member); | ||
| Z_GUARD_P(zv) &= ~ZEND_GUARD_PROPERTY_MASK; | ||
| zend_string_release(member); | ||
| return &Z_GUARD_P(zv); | ||
| } | ||
| /* we have to allocate uint32_t separately because ht->arData may be reallocated */ | ||
| ptr = (uint32_t*)emalloc(sizeof(uint32_t)); | ||
| *ptr = 0; | ||
| return (uint32_t*)zend_hash_add_new_ptr(guards, member, ptr); | ||
| uint32_t *result = (uint32_t*)zend_hash_add_new_ptr(guards, member, ptr); | ||
| zend_string_release(member); | ||
| return result; | ||
| } | ||
| /* }}} */ | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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'm quite afraid of 32 bit integer overflows for long running scripts here. It's a very good source of a heisenbug, especially if the counter overflows to 0 and a fiber and a main thread happen to suspend at the same place. With 64 bits I wouldn't worry.
After all the hash is also a zend_ulong, so, why not make this a zend_ulong too?
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.
Sounds reasonable 👍