From 4de78aff9fe8658fcb857f39f1f51e17ad168b08 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 28 Oct 2026 06:27:30 +0000 Subject: [PATCH 1/2] Fix GH-24445: Tracing JIT loses register-allocated CVs in catch blocks. Should also fix GH-24798 --- ext/opcache/jit/zend_jit_trace.c | 9 ++++++++ ext/opcache/tests/jit/gh24445.phpt | 36 ++++++++++++++++++++++++++++++ ext/opcache/tests/jit/gh24798.phpt | 33 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 ext/opcache/tests/jit/gh24445.phpt create mode 100644 ext/opcache/tests/jit/gh24798.phpt diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index b424934e4927..0656ef252740 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -3266,6 +3266,15 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t } if (count) { + for (i = 0; i < ssa->vars_count; i++) { + if (RA_HAS_REG(i) && + vars_op_array[i] && + vars_op_array[i]->last_try_catch && + ssa->vars[i].var < vars_op_array[i]->last_var && + !(RA_REG_FLAGS(i) & ZREG_LOAD)) { + RA_REG_FLAGS(i) |= ZREG_STORE; + } + } /* SSA resolution */ if (trace_buffer->stop == ZEND_JIT_TRACE_STOP_LOOP || trace_buffer->stop == ZEND_JIT_TRACE_STOP_RECURSIVE_CALL diff --git a/ext/opcache/tests/jit/gh24445.phpt b/ext/opcache/tests/jit/gh24445.phpt new file mode 100644 index 000000000000..e858855ec2c1 --- /dev/null +++ b/ext/opcache/tests/jit/gh24445.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-24445 (Tracing JIT: a catch reads an int local as undefined after an internal call throws) +--CREDITS-- +EJ-Research +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_loop=1 +opcache.jit_hot_func=1 +--FILE-- + +--EXPECT-- +int(1) diff --git a/ext/opcache/tests/jit/gh24798.phpt b/ext/opcache/tests/jit/gh24798.phpt new file mode 100644 index 000000000000..b0fd253de038 --- /dev/null +++ b/ext/opcache/tests/jit/gh24798.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-24798 (Tracing JIT loses register-allocated CVs when an exception is caught in the same function) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_loop=1 +opcache.jit_hot_func=1 +opcache.jit_hot_return=1 +opcache.jit_hot_side_exit=1 +--FILE-- +getMessage(), ' at $i = ', $i, PHP_EOL; + $s += 100; + } + } + return $s; +} +var_dump(f(5)); +?> +--EXPECT-- +DivisionByZeroError: Division by zero at $i = 2 +float(100) From 5d9947b1c1974d604256a531b602756b339226df Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 30 Oct 2026 21:43:27 +0000 Subject: [PATCH 2/2] (try) to apply suggestions --- ext/opcache/jit/zend_jit_trace.c | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 0656ef252740..871e170b3cd8 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -2827,6 +2827,21 @@ static void zend_jit_trace_use_var(int line, int var, int def, int use_chain, ze } } +static bool zend_jit_trace_op_in_try(const zend_op_array *op_array, const zend_op *opline) +{ + uint32_t op_num = opline - op_array->opcodes; + + for (uint32_t i = 0; i < op_array->last_try_catch; i++) { + const zend_try_catch_element *try_catch = &op_array->try_catch_array[i]; + + if (op_num >= try_catch->try_op + && op_num < (try_catch->finally_op ? try_catch->finally_op : try_catch->catch_op)) { + return true; + } + } + return false; +} + static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *trace_buffer, zend_ssa *ssa, uint32_t parent_trace, uint32_t exit_num) { const zend_op **ssa_opcodes = ((zend_tssa*)ssa)->tssa_opcodes; @@ -2838,6 +2853,7 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t int i, j, idx, count, level; zend_jit_reg_var *ra; const zend_op_array **vars_op_array; + zend_bitset store_vars; void *checkpoint; zend_jit_trace_stack_frame *frame; zend_jit_trace_stack *stack; @@ -2850,6 +2866,7 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t ra = zend_arena_calloc(&CG(arena), ssa->vars_count, sizeof(zend_jit_reg_var)); vars_op_array = zend_arena_calloc(&CG(arena), ssa->vars_count, sizeof(zend_op_array*)); memset(ZEND_VOIDP(vars_op_array), 0, sizeof(zend_op_array*) * ssa->vars_count); + store_vars = zend_arena_calloc(&CG(arena), zend_bitset_len(ssa->vars_count), ZEND_BITSET_ELM_SIZE); op_array = trace_buffer->op_array; jit_extension = @@ -2939,6 +2956,17 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t support_opline = 0; } + if (op_array->last_try_catch + && zend_jit_trace_op_in_try(op_array, opline) + && zend_may_throw(opline, ssa_op, op_array, ssa)) { + for (i = 0; i < op_array->last_var; i++) { + j = STACK_VAR(stack, i); + if (j >= 0 && RA_HAS_IVAL(j)) { + zend_bitset_incl(store_vars, j); + } + } + } + if (ssa_op->op1_use >= 0 && RA_HAS_IVAL(ssa_op->op1_use)) { if (!support_opline) { @@ -3268,9 +3296,7 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t if (count) { for (i = 0; i < ssa->vars_count; i++) { if (RA_HAS_REG(i) && - vars_op_array[i] && - vars_op_array[i]->last_try_catch && - ssa->vars[i].var < vars_op_array[i]->last_var && + zend_bitset_in(store_vars, i) && !(RA_REG_FLAGS(i) & ZREG_LOAD)) { RA_REG_FLAGS(i) |= ZREG_STORE; }