From 96d11a34741415b135cb87b959064d221d84c8ad Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:18:18 +0200 Subject: [PATCH] Fix GH-19065: Long match statement can segfault compiler during recursive 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. --- Zend/Optimizer/zend_ssa.c | 101 ++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/Zend/Optimizer/zend_ssa.c b/Zend/Optimizer/zend_ssa.c index 590df8155e395..bbd76922504e0 100644 --- a/Zend/Optimizer/zend_ssa.c +++ b/Zend/Optimizer/zend_ssa.c @@ -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,7 +788,7 @@ 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; @@ -795,15 +796,6 @@ static zend_result zend_ssa_rename(const zend_op_array *op_array, uint32_t build 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)); + 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; + 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]; + } } + 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) /* {{{ */ {