-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix GH-19065: Long match statement can segfault compiler during recursive SSA renaming #19083
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: PHP-8.3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…sive SSA renaming On some systems, like Alpine, the thread stack size is small by default. The last step of SSA construction involves variable renaming that is recursive, and also makes copies of their version of the renamed variables on the stack. This combination causes a stack overflow during compilation on Alpine. Triggerable for example with very long match statements. A stop-gap solution would be to use heap allocated arrays for the renamed variable list, but that would only delay the error as increasing the number of match arms increases the depth of the dominator tree, and will eventually run into the same issue. This patch transforms the algorithm into an iterative one. There are two states stored in a worklist stack: positive numbers indicate that the block still needs to undergo variable renaming. Negative numbers indicate that the block and its dominated children are already renamed. Because 0 is also a valid block number, we bias the block numbers by adding 1. To restore to the right variant when backtracking the "recursive" step, we index into an array pointing to the different variable renaming variants.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ | |
#include "zend_ssa.h" | ||
#include "zend_dump.h" | ||
#include "zend_inference.h" | ||
#include "zend_worklist.h" | ||
#include "Optimizer/zend_optimizer_internal.h" | ||
|
||
static bool dominates(const zend_basic_block *blocks, int a, int b) { | ||
|
@@ -787,23 +788,14 @@ ZEND_API int zend_ssa_rename_op(const zend_op_array *op_array, const zend_op *op | |
} | ||
/* }}} */ | ||
|
||
static zend_result zend_ssa_rename(const zend_op_array *op_array, uint32_t build_flags, zend_ssa *ssa, int *var, int n) /* {{{ */ | ||
static void zend_ssa_rename_in_block(const zend_op_array *op_array, uint32_t build_flags, zend_ssa *ssa, int *var, int n) /* {{{ */ | ||
{ | ||
zend_basic_block *blocks = ssa->cfg.blocks; | ||
zend_ssa_block *ssa_blocks = ssa->blocks; | ||
zend_ssa_op *ssa_ops = ssa->ops; | ||
int ssa_vars_count = ssa->vars_count; | ||
int i, j; | ||
zend_op *opline, *end; | ||
int *tmp = NULL; | ||
ALLOCA_FLAG(use_heap = 0); | ||
|
||
// FIXME: Can we optimize this copying out in some cases? | ||
if (blocks[n].next_child >= 0) { | ||
tmp = do_alloca(sizeof(int) * (op_array->last_var + op_array->T), use_heap); | ||
memcpy(tmp, var, sizeof(int) * (op_array->last_var + op_array->T)); | ||
var = tmp; | ||
} | ||
|
||
if (ssa_blocks[n].phis) { | ||
zend_ssa_phi *phi = ssa_blocks[n].phis; | ||
|
@@ -887,22 +879,91 @@ static zend_result zend_ssa_rename(const zend_op_array *op_array, uint32_t build | |
} | ||
|
||
ssa->vars_count = ssa_vars_count; | ||
} | ||
/* }}} */ | ||
|
||
j = blocks[n].children; | ||
while (j >= 0) { | ||
// FIXME: Tail call optimization? | ||
if (zend_ssa_rename(op_array, build_flags, ssa, var, j) == FAILURE) | ||
return FAILURE; | ||
j = blocks[j].next_child; | ||
} | ||
static zend_result zend_ssa_rename(const zend_op_array *op_array, uint32_t build_flags, zend_ssa *ssa, int *var, int n) | ||
{ | ||
/* The worklist contains block numbers, encoded as positive or negative value. | ||
* Positive values indicate that the variable rename still needs to happen for the block. | ||
* Negative values indicate the variable rename was done and all children were handled too. | ||
* In that case, we will clean up. | ||
* Because block 0 is valid, we bias the block numbers by adding 1 such that we can distinguish | ||
* positive and negative values in all cases. */ | ||
zend_worklist_stack work; | ||
ALLOCA_FLAG(work_use_heap); | ||
ZEND_WORKLIST_STACK_ALLOCA(&work, ssa->cfg.blocks_count, work_use_heap); | ||
zend_worklist_stack_push(&work, n + 1); | ||
|
||
/* This is used together with `save_vars` to backtrack the right version of the renamed variables to use. */ | ||
ALLOCA_FLAG(save_positions_use_heap); | ||
ALLOCA_FLAG(save_vars_use_heap); | ||
unsigned int save_vars_top = 0; | ||
unsigned int *save_positions = do_alloca(sizeof(unsigned int) * ssa->cfg.blocks_count, save_positions_use_heap); | ||
int **save_vars = do_alloca(sizeof(int *) * (ssa->cfg.blocks_count + 1), save_vars_use_heap); | ||
save_vars[0] = var; | ||
|
||
while (work.len) { | ||
n = zend_worklist_stack_pop(&work); | ||
|
||
/* Enter state: perform SSA variable rename */ | ||
if (n > 0) { | ||
n--; | ||
|
||
/* Push backtrack state */ | ||
zend_worklist_stack_push(&work, -(n + 1)); | ||
|
||
// FIXME: Can we optimize this copying out in some cases? | ||
int *new_var; | ||
if (ssa->cfg.blocks[n].next_child >= 0) { | ||
new_var = emalloc(sizeof(int) * (op_array->last_var + op_array->T)); | ||
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. We could still use alloca here (with a shared limit), as we free I think we need only one allocation per depth level, too, as the lifetime of each emalloc() here do not overlap for two blocks of the same depth. This is probably out of the scope of this bug fix however, but maybe we could do something like this:
Then we don't need backtracking, and we can remove |
||
memcpy(new_var, save_vars[save_vars_top], sizeof(int) * (op_array->last_var + op_array->T)); | ||
save_positions[n] = save_vars_top++; | ||
save_vars[save_vars_top] = new_var; | ||
} else { | ||
new_var = save_vars[save_vars_top]; | ||
save_positions[n] = save_vars_top; | ||
} | ||
|
||
zend_ssa_rename_in_block(op_array, build_flags, ssa, new_var, n); | ||
|
||
if (tmp) { | ||
free_alloca(tmp, use_heap); | ||
/* Push children in enter state */ | ||
unsigned int child_count = 0; | ||
int len_prior = work.len; | ||
int j = ssa->cfg.blocks[n].children; | ||
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. For leaves, can we jump directly to backtrack, and avoid pushing/poping |
||
while (j >= 0) { | ||
zend_worklist_stack_push(&work, j + 1); | ||
j = ssa->cfg.blocks[j].next_child; | ||
child_count++; | ||
} | ||
|
||
/* Reverse block order to maintain SSA variable number order given in previous PHP versions, | ||
* but the data structure doesn't allow reverse dominator tree traversal. */ | ||
for (unsigned int i = 0; i < child_count / 2; i++) { | ||
int tmp = work.buf[len_prior + i]; | ||
work.buf[len_prior + i] = work.buf[work.len - 1 - i]; | ||
work.buf[work.len - 1 - i] = tmp; | ||
} | ||
} | ||
/* Leave state: backtrack */ | ||
else { | ||
n = -n; | ||
n--; | ||
|
||
for (unsigned int i = save_vars_top, p = save_positions[n]; i > p; i--) { | ||
efree(save_vars[i]); | ||
} | ||
|
||
save_vars_top = save_positions[n]; | ||
Comment on lines
+953
to
+957
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 think this always frees exactly one save_vars or nothing: This could be simplified to
|
||
} | ||
} | ||
|
||
free_alloca(save_vars, save_vars_use_heap); | ||
free_alloca(save_positions, save_positions_use_heap); | ||
ZEND_WORKLIST_STACK_FREE_ALLOCA(&work, work_use_heap); | ||
|
||
return SUCCESS; | ||
} | ||
/* }}} */ | ||
|
||
ZEND_API zend_result zend_build_ssa(zend_arena **arena, const zend_script *script, const zend_op_array *op_array, uint32_t build_flags, zend_ssa *ssa) /* {{{ */ | ||
{ | ||
|
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.
Maybe it's possible to combine
save_vars
andsave_positions
somehow, but I haven't thought much of it yet. Probably via pointer tagging to indicate which arrays are reused and which are new allocations.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.
Maybe you could also put them together into a struct
Saves an allocation, not familiar enough with C though to know if this is actually faster
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 don't think that really matters, in the end the memory consumption is equal.