Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8.1k
Add VM reentry limit#5135
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
Uh oh!
There was an error while loading. Please reload this page.
Add VM reentry limit #5135
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| --TEST-- | ||
| VM reentry limit | ||
| --INI-- | ||
| zend.vm_reentry_limit=20 | ||
| --FILE-- | ||
| <?php | ||
| class Test1 { | ||
| public function __destruct() { | ||
| new Test1; | ||
| } | ||
| } | ||
| class Test2 { | ||
| public function __clone() { | ||
| clone $this; | ||
| } | ||
| } | ||
| try { | ||
| new Test1; | ||
| } catch (Error $e) { | ||
| echo $e, "\n"; | ||
| } | ||
| echo "\n"; | ||
| try { | ||
| clone new Test2; | ||
| } catch (Error $e) { | ||
| echo $e, "\n"; | ||
| } | ||
| ?> | ||
| --EXPECTF-- | ||
| Error: VM reentry limit of 20 reached. Infinite recursion? in %s:%d | ||
| Stack trace: | ||
| #0 %s(%d): Test1->__destruct() | ||
| #1 %s(%d): Test1->__destruct() | ||
| #2 %s(%d): Test1->__destruct() | ||
| #3 %s(%d): Test1->__destruct() | ||
| #4 %s(%d): Test1->__destruct() | ||
| #5 %s(%d): Test1->__destruct() | ||
| #6 %s(%d): Test1->__destruct() | ||
| #7 %s(%d): Test1->__destruct() | ||
| #8 %s(%d): Test1->__destruct() | ||
| #9 %s(%d): Test1->__destruct() | ||
| #10 %s(%d): Test1->__destruct() | ||
| #11 %s(%d): Test1->__destruct() | ||
| #12 %s(%d): Test1->__destruct() | ||
| #13 %s(%d): Test1->__destruct() | ||
| #14 %s(%d): Test1->__destruct() | ||
| #15 %s(%d): Test1->__destruct() | ||
| #16 %s(%d): Test1->__destruct() | ||
| #17 %s(%d): Test1->__destruct() | ||
| #18 %s(%d): Test1->__destruct() | ||
| #19 %s(%d): Test1->__destruct() | ||
| #20 %s(%d): Test1->__destruct() | ||
| #21 {main} | ||
| Error: VM reentry limit of 20 reached. Infinite recursion? in %s:%d | ||
| Stack trace: | ||
| #0 %s(%d): Test2->__clone() | ||
| #1 %s(%d): Test2->__clone() | ||
| #2 %s(%d): Test2->__clone() | ||
| #3 %s(%d): Test2->__clone() | ||
| #4 %s(%d): Test2->__clone() | ||
| #5 %s(%d): Test2->__clone() | ||
| #6 %s(%d): Test2->__clone() | ||
| #7 %s(%d): Test2->__clone() | ||
| #8 %s(%d): Test2->__clone() | ||
| #9 %s(%d): Test2->__clone() | ||
| #10 %s(%d): Test2->__clone() | ||
| #11 %s(%d): Test2->__clone() | ||
| #12 %s(%d): Test2->__clone() | ||
| #13 %s(%d): Test2->__clone() | ||
| #14 %s(%d): Test2->__clone() | ||
| #15 %s(%d): Test2->__clone() | ||
| #16 %s(%d): Test2->__clone() | ||
| #17 %s(%d): Test2->__clone() | ||
| #18 %s(%d): Test2->__clone() | ||
| #19 %s(%d): Test2->__clone() | ||
| #20 %s(%d): Test2->__clone() | ||
| #21 {main} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -388,6 +388,12 @@ zend.exception_ignore_args = On | ||
| ; of sensitive information in stack traces. | ||
| zend.exception_string_param_max_len = 0 | ||
| ; Limit for recursion via VM reentry, used to prevent stack overflow. | ||
| ; This only affects recursion through magic method calls and similar mechanism. | ||
| ; Some profiling, debugging or APM extensions might make this limit apply to plain | ||
| ; recursion as well, in which case you may wish to raise it. | ||
| ;zend.vm_reentry_limit = 1000 | ||
| ||
| ;;;;;;;;;;;;;;;;; | ||
| ; Miscellaneous ; | ||
| ;;;;;;;;;;;;;;;;; | ||
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.
Would a small minimum value make sense?
Right now, it's possible to set it to 0, which would prevent __destruct (etc) from being called at all, e.g. during request cleanup.
Values such as 0 may lead to false positive bug reports from users who misconfigured it, e.g. when setting this when debugging other issues and misreading the documentation.
LGTM other than the question.
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 find this tricky especially with extensions that overwrite
zend_execute_exthe limit is certainly going to be hit by larger frameworks and template engines regularly, whereas for the goto vm mode it will never go very deep.I believe 1000 could be too small.
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.
Another suggestion: adding some form of logging (of a severity not catchable by set_error_handler, because the error handler itself is re-entering?) before throwing an exception might help avoid confusion with
catchblocks that don't perform any logging.Without the additional logging, there would be no obvious reason why recursive AST processing would potentially silently fail to process the entire AST on some system configurations.
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.
Extensions that overwrite zend_execute_ex should set a higher limit for this ini setting, or disable it entirely. A higher limit doesn't really make sense for a default config (probably a much lower one would also do).
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.
@nikic i am thinking in terms of developer experience, this segfault is something users can hardly understand (as you mentioned in your mailing list post). As such i would certainly like to not disable it when my extension is active.
With just 1000, all extensions just disable it magically and users run into these segfaults again, which are more likely to happen when you have an extension installed that uses a zend_execute_ex overwrite.
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.
@beberlei So first of all, I do hope that your extension (and other APM extensions) will be able to avoid the VM recursion based on the new hooks that Levi & co were working on. But assuming that doesn't pan out (in time), you don't necessarily have to disable the option (that's just the conservative choice, that gives you exactly the behavior you get on PHP 7), you can also bump the limit to a larger value, based on an analysis of stack usage if your extension is enabled. I don't think that it makes sense to increase the default value further (I'd say 1000 is already very conservative), because it's unlikely to be useful without such an extension in play, but increases the chances of missing stack overflows (if you bump to say 10000).
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.
@TysonAndre I wouldn't want to introduce a special error handling mechanism just for this functionality -- but possibly it would be better to make this a fatal error instead of an exception? I mainly went with the exception because "I can" and it includes a stack trace that is likely helpful when dealing with infinite recursion.
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.
That would be my preference - terminating cleanly instead of throwing an error would avoid a lot of confusing edge cases in internals and for end users. The programs would already be prone to terminating if they recursed that deeply, except for edge cases (e.g. __toString on a deeply nested tree structure).
My other concern was whether this breaks assumptions in opcache or for end users (e.g. certain opcode types will never throw at runtime, e.g. casting to string)
Error handling properly might be an issue
Also, if you hit the recursion limit, then __destruct() can't get called when an object is freed, which breaks other assumptions you might have about unserialize(), or assumptions users make about RAII or other patterns (e.g. connection or memory leaks)
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.
@nikic yes, i have tested levis new API extensively already and it will make this problem go away.