From 1613bc63e57c5d5f8c0ff65cbb6c6ff6177b05be Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 16:09:43 +0100 Subject: [PATCH 01/15] PoC --- Zend/tests/ns_global_func_assumption_001.phpt | 17 +++ Zend/tests/ns_global_func_assumption_002.inc | 8 ++ Zend/tests/ns_global_func_assumption_002.phpt | 22 +++ .../ns_global_func_assumption_002_shadow.inc | 7 + Zend/zend_compile.c | 133 +++++++++++++++++- Zend/zend_compile.h | 10 +- Zend/zend_execute_API.c | 4 + Zend/zend_globals.h | 7 + Zend/zend_vm_def.h | 40 +++++- Zend/zend_vm_execute.h | 80 ++++++++++- .../tests/ns_global_func_assumption_001.inc | 7 + .../tests/ns_global_func_assumption_001.phpt | 59 ++++++++ 12 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 Zend/tests/ns_global_func_assumption_001.phpt create mode 100644 Zend/tests/ns_global_func_assumption_002.inc create mode 100644 Zend/tests/ns_global_func_assumption_002.phpt create mode 100644 Zend/tests/ns_global_func_assumption_002_shadow.inc create mode 100644 ext/opcache/tests/ns_global_func_assumption_001.inc create mode 100644 ext/opcache/tests/ns_global_func_assumption_001.phpt diff --git a/Zend/tests/ns_global_func_assumption_001.phpt b/Zend/tests/ns_global_func_assumption_001.phpt new file mode 100644 index 000000000000..2a6f0a061f70 --- /dev/null +++ b/Zend/tests/ns_global_func_assumption_001.phpt @@ -0,0 +1,17 @@ +--TEST-- +NS global function assumption: basic optimization +--FILE-- + +--EXPECT-- +int(5) +string(8) "shadow:5" diff --git a/Zend/tests/ns_global_func_assumption_002_shadow.inc b/Zend/tests/ns_global_func_assumption_002_shadow.inc new file mode 100644 index 000000000000..268babc02c9c --- /dev/null +++ b/Zend/tests/ns_global_func_assumption_002_shadow.inc @@ -0,0 +1,7 @@ +type == ZEND_USER_FUNCTION); + + /* Check cache first. */ + uintptr_t cache_key = (uintptr_t)func; + zend_function *deopt = zend_hash_index_find_ptr(&EG(ns_deoptimized_functions), cache_key); + if (deopt) { + return deopt; + } + + /* Temporarily remove user functions from the same file to avoid + * redeclaration errors during recompilation. */ + HashTable saved_functions; + zend_hash_init(&saved_functions, 8, NULL, NULL, 0); + + const zend_string *filename = func->op_array.filename; + zend_string *key; + zval *zv; + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(EG(function_table), key, zv) { + zend_function *f = Z_PTR_P(zv); + if (f->type == ZEND_USER_FUNCTION && f->op_array.filename == filename) { + zend_hash_add_new_ptr(&saved_functions, key, f); + } + } ZEND_HASH_FOREACH_END(); + + dtor_func_t orig_dtor = EG(function_table)->pDestructor; + EG(function_table)->pDestructor = NULL; + ZEND_HASH_MAP_FOREACH_STR_KEY(&saved_functions, key) { + zend_hash_del(EG(function_table), key); + } ZEND_HASH_FOREACH_END(); + EG(function_table)->pDestructor = orig_dtor; + + /* Compile the file without NS global assumptions. */ + uint32_t orig_compiler_options = CG(compiler_options); + CG(compiler_options) |= ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION; + CG(compiler_options) |= ZEND_COMPILE_IGNORE_OTHER_FILES; + CG(compiler_options) |= ZEND_COMPILE_DELAYED_BINDING; + + zend_file_handle file_handle; + zend_stream_init_filename_ex(&file_handle, func->op_array.filename); + zend_op_array *main_op_array = compile_file(&file_handle, ZEND_INCLUDE); + zend_destroy_file_handle(&file_handle); + + CG(compiler_options) = orig_compiler_options; + + /* Find the deoptimized function by name. */ + deopt = NULL; + if (main_op_array && func->op_array.function_name) { + zend_string *lc_name = zend_string_tolower(func->op_array.function_name); + deopt = zend_hash_find_ptr(EG(function_table), lc_name); + zend_string_release(lc_name); + } + + /* Remove newly compiled functions and restore originals. */ + EG(function_table)->pDestructor = NULL; + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(&saved_functions, key, zv) { + zend_hash_update_ptr(EG(function_table), key, Z_PTR_P(zv)); + } ZEND_HASH_FOREACH_END(); + EG(function_table)->pDestructor = orig_dtor; + zend_hash_destroy(&saved_functions); + + if (main_op_array) { + destroy_op_array(main_op_array); + efree(main_op_array); + } + + /* Cache the result. */ + if (deopt) { + zend_hash_index_add_new_ptr(&EG(ns_deoptimized_functions), cache_key, deopt); + } + + return deopt; +} + static zend_never_inline ZEND_COLD ZEND_NORETURN void do_bind_function_error(const zend_string *lcname, const zend_op_array *op_array, bool compile_time) /* {{{ */ { const zval *zv = zend_hash_find_known_hash(compile_time ? CG(function_table) : EG(function_table), lcname); @@ -1315,6 +1410,7 @@ ZEND_API zend_result do_bind_function(zend_function *func, const zval *lcname) / zend_string_addref(func->common.function_name); } zend_observer_function_declared_notify(&func->op_array, Z_STR_P(lcname)); + zend_check_ns_function_shadow(Z_STR_P(lcname)); return SUCCESS; } /* }}} */ @@ -5424,13 +5520,45 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) if (zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert") && !is_callable_convert) { zend_compile_assert(result, zend_ast_get_list(args_ast), Z_STR(name_node.u.constant), NULL, ast->lineno, type); - } else { - zend_compile_ns_call(result, &name_node, args_ast, ast->lineno, type); + return; } + + if (!is_callable_convert && !(CG(compiler_options) & ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION)) { + /* Check if the unqualified name matches a known global function + * that is not shadowed by a NS-local function at compile time. */ + zend_string *orig_name = zend_ast_get_str(name_ast); + zend_string *lc_orig_name = zend_string_tolower(orig_name); + zend_string *lc_ns_name = zend_string_tolower(Z_STR(name_node.u.constant)); + const zend_function *global_fbc = zend_hash_find_ptr(CG(function_table), lc_orig_name); + bool ns_func_exists = zend_hash_exists(CG(function_table), lc_ns_name) + || zend_have_seen_symbol(lc_ns_name, ZEND_SYMBOL_FUNCTION); + zend_string_release(lc_ns_name); + + if (global_fbc + && !ns_func_exists + && !ZEND_USER_CODE(global_fbc->type) + && !zend_string_equals_literal(lc_orig_name, "call_user_func") + && !zend_string_equals_literal(lc_orig_name, "in_array") + && !zend_compile_ignore_function(global_fbc, CG(active_op_array)->filename)) { + /* Assume the unqualified call resolves to the global function. + * Replace NS-qualified name with unqualified name and fall + * through to the fully-qualified compilation path. */ + zval_ptr_dtor(&name_node.u.constant); + ZVAL_STR_COPY(&name_node.u.constant, orig_name); + zend_string_release(lc_orig_name); + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_NS_GLOBAL_ASSUMED; + goto resolve_as_global; + } + + zend_string_release(lc_orig_name); + } + + zend_compile_ns_call(result, &name_node, args_ast, ast->lineno, type); return; } } +resolve_as_global:; { const zval *name = &name_node.u.constant; zend_string *lcname = zend_string_tolower(Z_STR_P(name)); @@ -8898,6 +9026,7 @@ static zend_op_array *zend_compile_func_decl_ex( CG(zend_lineno) = decl->start_lineno; do_bind_function_error(lcname, op_array, true); } + zend_check_ns_function_shadow(lcname); } /* put the implicit return on the really last line */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 0b38084a107c..2b5211f66657 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -413,10 +413,12 @@ typedef struct _zend_oparray_context { /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ /* | | | */ -/* Function Flags 2 (fn_flags2) (unused: 0-31) | | | */ +/* Function Flags 2 (fn_flags2) (unused: 1-31) | | | */ /* ============================ | | | */ /* | | | */ -/* #define ZEND_ACC2_EXAMPLE (1 << 0) | X | | */ +/* op_array was compiled assuming unqualified calls | | | */ +/* to global functions resolve globally | | | */ +#define ZEND_ACC2_NS_GLOBAL_ASSUMED (1 << 0) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) @@ -964,6 +966,7 @@ ZEND_API zend_result zend_execute_script(int type, zval *retval, zend_file_handl ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle); ZEND_API void init_op_array(zend_op_array *op_array, zend_function_type type, int initial_ops_size); ZEND_API void destroy_op_array(zend_op_array *op_array); +ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func); ZEND_API void zend_destroy_static_vars(zend_op_array *op_array); ZEND_API void zend_destroy_file_handle(zend_file_handle *file_handle); ZEND_API void zend_cleanup_mutable_class_data(zend_class_entry *ce); @@ -1311,6 +1314,9 @@ END_EXTERN_C() /* ignore observer notifications, e.g. to manually notify afterwards in a post-processing step after compilation */ #define ZEND_COMPILE_IGNORE_OBSERVER (1<<18) +/* Disable NS global function assumption optimization (used during deopt recompilation) */ +#define ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION (1<<19) + /* The default value for CG(compiler_options) */ #define ZEND_COMPILE_DEFAULT ZEND_COMPILE_HANDLE_OP_ARRAY diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index dbd2a9039cfc..f7d922c4412f 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -204,6 +204,9 @@ void init_executor(void) /* {{{ */ zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); + EG(ns_global_func_generation) = 0; + zend_hash_init(&EG(ns_deoptimized_functions), 0, NULL, ZEND_FUNCTION_DTOR, 0); + EG(active) = 1; } /* }}} */ @@ -516,6 +519,7 @@ void shutdown_executor(void) /* {{{ */ } zend_hash_destroy(&EG(callable_convert_cache)); + zend_hash_destroy(&EG(ns_deoptimized_functions)); } #if ZEND_DEBUG diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 31f54cd8284b..6f6e336d3627 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -321,6 +321,13 @@ struct _zend_executor_globals { HashTable callable_convert_cache; + /* Bumped when a namespaced function shadowing a global function is declared. + * Used to invalidate NS global function assumptions made during compilation. */ + uint32_t ns_global_func_generation; + + /* Cache of deoptimized op_arrays, keyed by original op_array pointer (uintptr_t). */ + HashTable ns_deoptimized_functions; + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 86708f8c97a2..5c44e91de9a7 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3912,6 +3912,17 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) } CACHE_PTR(opline->result.num, fbc); } + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + } + } + call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4098,8 +4109,21 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT) CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } + call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; @@ -4117,8 +4141,20 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2 fbc = Z_PTR(EG(function_table)->arData[Z_EXTRA_P(RT_CONSTANT(opline, opline->op2))].val); CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cbfae90802cf..660d748e8664 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4027,6 +4027,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } CACHE_PTR(opline->result.num, fbc); } + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + } + } + call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4142,8 +4153,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } + call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; @@ -4161,8 +4185,20 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I fbc = Z_PTR(EG(function_table)->arData[Z_EXTRA_P(RT_CONSTANT(opline, opline->op2))].val); CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; @@ -56631,6 +56667,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } CACHE_PTR(opline->result.num, fbc); } + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + } + } + call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -56746,8 +56793,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } + call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; @@ -56765,8 +56825,20 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F fbc = Z_PTR(EG(function_table)->arData[Z_EXTRA_P(RT_CONSTANT(opline, opline->op2))].val); CACHE_PTR(opline->result.num, fbc); } + uint32_t used_stack = opline->op1.num; + if (UNEXPECTED(EG(ns_global_func_generation) > 0 + && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (deopt) { + fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } + } call = _zend_vm_stack_push_call_frame_ex( - opline->op1.num, ZEND_CALL_NESTED_FUNCTION, + used_stack, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; diff --git a/ext/opcache/tests/ns_global_func_assumption_001.inc b/ext/opcache/tests/ns_global_func_assumption_001.inc new file mode 100644 index 000000000000..a0d5b43d665b --- /dev/null +++ b/ext/opcache/tests/ns_global_func_assumption_001.inc @@ -0,0 +1,7 @@ + +--EXPECTF-- +$_main: + ; (lines=6, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.php:%s +0000 INIT_FCALL 0 96 string("ns\\test") +0001 DO_UCALL +0002 INCLUDE_OR_EVAL (require) string("%sns_global_func_assumption_001.inc") +0003 INIT_FCALL 0 96 string("ns\\test") +0004 DO_UCALL +0005 RETURN int(1) + +Ns\test: + ; (lines=4, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.php:%s +0000 INIT_FCALL 1 %d string("var_dump") +0001 SEND_VAL int(11) 1 +0002 DO_ICALL +0003 RETURN null +int(11) + +$_main: + ; (lines=1, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.inc:%s +0000 RETURN int(1) + +Ns\strlen: + ; (lines=4, args=1, vars=1, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.inc:%s +0000 CV0($string) = RECV 1 +0001 T2 = STRLEN CV0($string) +0002 T1 = ADD T2 T2 +0003 RETURN T1 +int(22) From 727d861eac2f12242f6ad341b4e28028bb586088 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 16:49:23 +0100 Subject: [PATCH 02/15] Drop ZEND_FRAMELESS_JMP --- Zend/zend_compile.c | 59 ++++++++++++++------------------------------- Zend/zend_compile.h | 1 - 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7d4c8710d280..077aff2cdd02 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -343,7 +343,6 @@ void zend_oparray_context_begin(zend_oparray_context *prev_context, zend_op_arra CG(context).has_assigned_to_http_response_header = false; CG(context).brk_cont_array = NULL; CG(context).labels = NULL; - CG(context).in_jmp_frameless_branch = false; CG(context).active_property_info_name = NULL; CG(context).active_property_hook_kind = (zend_property_hook_kind)-1; } @@ -4913,28 +4912,25 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast { int name_constants = zend_add_ns_func_name_literal(Z_STR(name_node->u.constant)); - /* Find frameless function with same name. */ - const zend_function *frameless_function = NULL; - if (args_ast->kind != ZEND_AST_CALLABLE_CONVERT - && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast)) - /* Avoid blowing up op count with nested frameless branches. */ - && !CG(context).in_jmp_frameless_branch) { + /* When compiling without the deoptimization flag, use frameless calls + * directly. If a shadow is later declared, the function will be + * deoptimized and recompiled without frameless. */ + if (!(CG(compiler_options) & ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION) + && args_ast->kind != ZEND_AST_CALLABLE_CONVERT + && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) { + zend_string *lc_ns_name = Z_STR_P(CT_CONSTANT_EX(CG(active_op_array), name_constants + 1)); zend_string *lc_func_name = Z_STR_P(CT_CONSTANT_EX(CG(active_op_array), name_constants + 2)); - frameless_function = zend_hash_find_ptr(CG(function_table), lc_func_name); - } - - /* Check whether any frameless handler may actually be used. */ - uint32_t jmp_fl_opnum = 0; - const zend_frameless_function_info *frameless_function_info = NULL; - if (frameless_function) { - frameless_function_info = find_frameless_function_info(zend_ast_get_list(args_ast), frameless_function, type); - if (frameless_function_info) { - CG(context).in_jmp_frameless_branch = true; - znode op1; - op1.op_type = IS_CONST; - ZVAL_COPY(&op1.u.constant, CT_CONSTANT_EX(CG(active_op_array), name_constants + 1)); - jmp_fl_opnum = get_next_op_number(); - zend_emit_op(NULL, ZEND_JMP_FRAMELESS, &op1, NULL); + if (!zend_hash_exists(CG(function_table), lc_ns_name) + && !zend_have_seen_symbol(lc_ns_name, ZEND_SYMBOL_FUNCTION)) { + const zend_function *frameless_function = zend_hash_find_ptr(CG(function_table), lc_func_name); + if (frameless_function) { + const zend_frameless_function_info *info = find_frameless_function_info(zend_ast_get_list(args_ast), frameless_function, type); + if (info) { + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_NS_GLOBAL_ASSUMED; + zend_compile_frameless_icall_ex(result, zend_ast_get_list(args_ast), frameless_function, info, type); + return; + } + } } } @@ -4945,25 +4941,6 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast opline->op2.constant = name_constants; opline->result.num = zend_alloc_cache_slot(); zend_compile_call_common(result, args_ast, NULL, lineno, type); - - /* Compile frameless call. */ - if (frameless_function_info) { - CG(zend_lineno) = lineno; - - uint32_t jmp_end_opnum = zend_emit_jump(0); - uint32_t jmp_fl_target = get_next_op_number(); - - uint32_t flf_icall_opnum = zend_compile_frameless_icall_ex(NULL, zend_ast_get_list(args_ast), frameless_function, frameless_function_info, type); - - zend_op *jmp_fl = &CG(active_op_array)->opcodes[jmp_fl_opnum]; - jmp_fl->op2.opline_num = jmp_fl_target; - jmp_fl->extended_value = zend_alloc_cache_slot(); - zend_op *flf_icall = &CG(active_op_array)->opcodes[flf_icall_opnum]; - SET_NODE(flf_icall->result, result); - zend_update_jump_target_to_next(jmp_end_opnum); - - CG(context).in_jmp_frameless_branch = false; - } } /* }}} */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 2b5211f66657..bb57bc69cc78 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -207,7 +207,6 @@ typedef struct _zend_oparray_context { HashTable *labels; zend_string *active_property_info_name; zend_property_hook_kind active_property_hook_kind; - bool in_jmp_frameless_branch; bool has_assigned_to_http_response_header; } zend_oparray_context; From 0fe76344a3ff58109ae1d3a5a94b700535368983 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 17:16:11 +0100 Subject: [PATCH 03/15] Fix tests --- .../tests/first_class_callable/constexpr/namespace_004.phpt | 6 ++++++ Zend/zend_compile.c | 2 +- ext/opcache/tests/ns_global_func_assumption_001.phpt | 4 ++-- sapi/phpdbg/tests/print_001.phpt | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Zend/tests/first_class_callable/constexpr/namespace_004.phpt b/Zend/tests/first_class_callable/constexpr/namespace_004.phpt index 6fe99593bf95..7908dbe6d52c 100644 --- a/Zend/tests/first_class_callable/constexpr/namespace_004.phpt +++ b/Zend/tests/first_class_callable/constexpr/namespace_004.phpt @@ -1,5 +1,11 @@ --TEST-- Allow defining FCC in const expressions in a namespace with function matching a global function later. +--SKIPIF-- + --FILE-- type) && !zend_string_equals_literal(lc_orig_name, "call_user_func") - && !zend_string_equals_literal(lc_orig_name, "in_array") + && !zend_string_equals_literal(lc_orig_name, "call_user_func_array") && !zend_compile_ignore_function(global_fbc, CG(active_op_array)->filename)) { /* Assume the unqualified call resolves to the global function. * Replace NS-qualified name with unqualified name and fall diff --git a/ext/opcache/tests/ns_global_func_assumption_001.phpt b/ext/opcache/tests/ns_global_func_assumption_001.phpt index 5ea797a416a7..0705ca4453f7 100644 --- a/ext/opcache/tests/ns_global_func_assumption_001.phpt +++ b/ext/opcache/tests/ns_global_func_assumption_001.phpt @@ -25,10 +25,10 @@ $_main: ; (lines=6, args=0, vars=0, tmps=%d) ; (after optimizer) ; %sns_global_func_assumption_001.php:%s -0000 INIT_FCALL 0 96 string("ns\\test") +0000 INIT_FCALL 0 %d string("ns\\test") 0001 DO_UCALL 0002 INCLUDE_OR_EVAL (require) string("%sns_global_func_assumption_001.inc") -0003 INIT_FCALL 0 96 string("ns\\test") +0003 INIT_FCALL 0 %d string("ns\\test") 0004 DO_UCALL 0005 RETURN int(1) diff --git a/sapi/phpdbg/tests/print_001.phpt b/sapi/phpdbg/tests/print_001.phpt index 031b4d5a961b..6bab58a58ea4 100644 --- a/sapi/phpdbg/tests/print_001.phpt +++ b/sapi/phpdbg/tests/print_001.phpt @@ -29,9 +29,9 @@ Foo\Bar::Foo: ; (lines=5, args=1, vars=1, tmps=%d) ; %s:5-7 L0005 0000 CV0($bar) = RECV 1 -L0006 0001 INIT_NS_FCALL_BY_NAME 1 string("Foo\\var_dump") -L0006 0002 SEND_VAR_EX CV0($bar) 1 -L0006 0003 DO_FCALL +L0006 0001 INIT_FCALL 1 %d string("var_dump") +L0006 0002 SEND_VAR CV0($bar) 1 +L0006 0003 DO_ICALL L0007 0004 RETURN null Foo\Bar::baz: From 06a3c7e906332b9cd499b8847b31968dab488f25 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 17:47:02 +0100 Subject: [PATCH 04/15] Fix uninitialized fn_flags2 --- Zend/zend_API.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 89333d89af9d..24992389d946 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3042,6 +3042,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend internal_function->function_name = zend_string_init_interned(ptr->fname, fname_len, 1); internal_function->scope = scope; internal_function->prototype = NULL; + internal_function->fn_flags2 = 0; internal_function->prop_info = NULL; internal_function->attributes = NULL; internal_function->frameless_function_infos = ptr->frameless_function_infos; From 3b57f2fc4d093f4d3a195494094739e4987ef5b5 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 21:31:51 +0100 Subject: [PATCH 05/15] Testing --- benchmark/benchmark.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/benchmark.php b/benchmark/benchmark.php index 0c2ac4c6010a..12a12ff76121 100644 --- a/benchmark/benchmark.php +++ b/benchmark/benchmark.php @@ -31,8 +31,8 @@ function main() { $data['Zend/bench.php JIT'] = runBench(true); $data['Symfony Demo 2.2.3'] = runSymfonyDemo(false); $data['Symfony Demo 2.2.3 JIT'] = runSymfonyDemo(true); - $data['Wordpress 6.2'] = runWordpress(false); - $data['Wordpress 6.2 JIT'] = runWordpress(true); + // $data['Wordpress 6.2'] = runWordpress(false); + // $data['Wordpress 6.2 JIT'] = runWordpress(true); $result = json_encode($data, JSON_PRETTY_PRINT) . "\n"; fwrite(STDOUT, $result); From 2a3d9ad7e7f98da0900a13ceda7ad09e50edd755 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 21:47:14 +0100 Subject: [PATCH 06/15] Renames --- Zend/zend_compile.c | 19 +++++++++---------- Zend/zend_compile.h | 10 +++++----- Zend/zend_execute_API.c | 6 +++--- Zend/zend_globals.h | 6 +++--- Zend/zend_vm_def.h | 9 +++------ Zend/zend_vm_execute.h | 18 ++++++------------ 6 files changed, 29 insertions(+), 39 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 5ab8130fe95e..bee7081670f2 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1291,21 +1291,20 @@ static void zend_check_ns_function_shadow(const zend_string *lcname) size_t unqualified_len = ZSTR_LEN(lcname) - (unqualified - ZSTR_VAL(lcname)); zend_string *global_name = zend_string_init(unqualified, unqualified_len, 0); if (zend_hash_exists(CG(function_table), global_name)) { - EG(ns_global_func_generation)++; + EG(num_shadowed_global_funcs)++; } zend_string_release(global_name); } /* Recompile a function without NS global assumptions. * Returns the deoptimized function or NULL on failure. - * The result is cached in EG(ns_deoptimized_functions). */ + * The result is cached in EG(deoptimized_funcs). */ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) { ZEND_ASSERT(func->type == ZEND_USER_FUNCTION); /* Check cache first. */ - uintptr_t cache_key = (uintptr_t)func; - zend_function *deopt = zend_hash_index_find_ptr(&EG(ns_deoptimized_functions), cache_key); + zend_function *deopt = zend_hash_find_ptr(&EG(deoptimized_funcs), func->common.function_name); if (deopt) { return deopt; } @@ -1334,7 +1333,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) /* Compile the file without NS global assumptions. */ uint32_t orig_compiler_options = CG(compiler_options); - CG(compiler_options) |= ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION; + CG(compiler_options) |= ZEND_COMPILE_DEOPTIMIZED; CG(compiler_options) |= ZEND_COMPILE_IGNORE_OTHER_FILES; CG(compiler_options) |= ZEND_COMPILE_DELAYED_BINDING; @@ -1368,7 +1367,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) /* Cache the result. */ if (deopt) { - zend_hash_index_add_new_ptr(&EG(ns_deoptimized_functions), cache_key, deopt); + zend_hash_add_new_ptr(&EG(deoptimized_funcs), deopt->common.function_name, deopt); } return deopt; @@ -4915,7 +4914,7 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast /* When compiling without the deoptimization flag, use frameless calls * directly. If a shadow is later declared, the function will be * deoptimized and recompiled without frameless. */ - if (!(CG(compiler_options) & ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION) + if (!(CG(compiler_options) & ZEND_COMPILE_DEOPTIMIZED) && args_ast->kind != ZEND_AST_CALLABLE_CONVERT && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) { zend_string *lc_ns_name = Z_STR_P(CT_CONSTANT_EX(CG(active_op_array), name_constants + 1)); @@ -4926,7 +4925,7 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast if (frameless_function) { const zend_frameless_function_info *info = find_frameless_function_info(zend_ast_get_list(args_ast), frameless_function, type); if (info) { - CG(active_op_array)->fn_flags2 |= ZEND_ACC2_NS_GLOBAL_ASSUMED; + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_ASSUMPTIONS; zend_compile_frameless_icall_ex(result, zend_ast_get_list(args_ast), frameless_function, info, type); return; } @@ -5500,7 +5499,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) return; } - if (!is_callable_convert && !(CG(compiler_options) & ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION)) { + if (!is_callable_convert && !(CG(compiler_options) & ZEND_COMPILE_DEOPTIMIZED)) { /* Check if the unqualified name matches a known global function * that is not shadowed by a NS-local function at compile time. */ zend_string *orig_name = zend_ast_get_str(name_ast); @@ -5523,7 +5522,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) zval_ptr_dtor(&name_node.u.constant); ZVAL_STR_COPY(&name_node.u.constant, orig_name); zend_string_release(lc_orig_name); - CG(active_op_array)->fn_flags2 |= ZEND_ACC2_NS_GLOBAL_ASSUMED; + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_ASSUMPTIONS; goto resolve_as_global; } diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index bb57bc69cc78..ace214f2fa6f 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -415,9 +415,9 @@ typedef struct _zend_oparray_context { /* Function Flags 2 (fn_flags2) (unused: 1-31) | | | */ /* ============================ | | | */ /* | | | */ -/* op_array was compiled assuming unqualified calls | | | */ -/* to global functions resolve globally | | | */ -#define ZEND_ACC2_NS_GLOBAL_ASSUMED (1 << 0) /* | X | | */ +/* op_array was compiled assuming possibly global calls | | | */ +/* are necessarily global | | | */ +#define ZEND_ACC2_ASSUMPTIONS (1 << 0) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) @@ -1313,8 +1313,8 @@ END_EXTERN_C() /* ignore observer notifications, e.g. to manually notify afterwards in a post-processing step after compilation */ #define ZEND_COMPILE_IGNORE_OBSERVER (1<<18) -/* Disable NS global function assumption optimization (used during deopt recompilation) */ -#define ZEND_COMPILE_NO_NS_GLOBAL_ASSUMPTION (1<<19) +/* Disable assumption that any possible global function call is global */ +#define ZEND_COMPILE_DEOPTIMIZED (1<<19) /* The default value for CG(compiler_options) */ #define ZEND_COMPILE_DEFAULT ZEND_COMPILE_HANDLE_OP_ARRAY diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index f7d922c4412f..31c772e64ecf 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -204,8 +204,8 @@ void init_executor(void) /* {{{ */ zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); - EG(ns_global_func_generation) = 0; - zend_hash_init(&EG(ns_deoptimized_functions), 0, NULL, ZEND_FUNCTION_DTOR, 0); + EG(num_shadowed_global_funcs) = 0; + zend_hash_init(&EG(deoptimized_funcs), 0, NULL, ZEND_FUNCTION_DTOR, 0); EG(active) = 1; } @@ -519,7 +519,7 @@ void shutdown_executor(void) /* {{{ */ } zend_hash_destroy(&EG(callable_convert_cache)); - zend_hash_destroy(&EG(ns_deoptimized_functions)); + zend_hash_destroy(&EG(deoptimized_funcs)); } #if ZEND_DEBUG diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 6f6e336d3627..c822263e7d7d 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -323,10 +323,10 @@ struct _zend_executor_globals { /* Bumped when a namespaced function shadowing a global function is declared. * Used to invalidate NS global function assumptions made during compilation. */ - uint32_t ns_global_func_generation; + uint32_t num_shadowed_global_funcs; - /* Cache of deoptimized op_arrays, keyed by original op_array pointer (uintptr_t). */ - HashTable ns_deoptimized_functions; + /* Cache of deoptimized op_arrays, keyed by function name. */ + HashTable deoptimized_funcs; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 5c44e91de9a7..34e56131df39 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3912,8 +3912,7 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -4110,8 +4109,7 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT) } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -4142,8 +4140,7 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2 CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 660d748e8664..a260acf19300 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4027,8 +4027,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -4154,8 +4153,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -4186,8 +4184,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -56667,8 +56664,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -56794,8 +56790,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; @@ -56826,8 +56821,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(ns_global_func_generation) > 0 - && (fbc->common.fn_flags2 & ZEND_ACC2_NS_GLOBAL_ASSUMED))) { + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { zend_function *deopt = zend_get_deoptimized_function(fbc); if (deopt) { fbc = deopt; From eea9c863e6588c97a5d390829af52a69bd277c72 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Mar 2026 21:56:17 +0100 Subject: [PATCH 07/15] Outline zend_fetch_deoptimized_func() --- Zend/zend_execute.c | 21 ++++++++++++++ Zend/zend_vm_def.h | 32 ++++----------------- Zend/zend_vm_execute.h | 64 ++++++++---------------------------------- 3 files changed, 39 insertions(+), 78 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 37278c5cb9a2..491eb82d93b2 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5897,6 +5897,27 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint /* This callback disables optimization of "vm_stack_data" variable in VM */ ZEND_API void (ZEND_FASTCALL *zend_touch_vm_stack_data)(void *vm_stack_data) = NULL; +static zend_never_inline ZEND_COLD void zend_fetch_deoptimized_func(zend_function **fbc_ptr, uint32_t *used_stack OPLINE_DC) +{ + zend_function *fbc = *fbc_ptr; + if (!(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + return; + } + + zend_function *deopt = zend_get_deoptimized_function(fbc); + if (!deopt) { + return; + } + + *fbc_ptr = fbc = deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } + if (used_stack) { + *used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); + } +} + #include "zend_vm_execute.h" ZEND_API zend_result zend_set_user_opcode_handler(zend_uchar opcode, user_opcode_handler_t handler) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 34e56131df39..986f14207a83 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3912,14 +3912,8 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, @@ -4109,15 +4103,8 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT) } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -4140,15 +4127,8 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2 CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index a260acf19300..b090d1c338b2 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4027,14 +4027,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, @@ -4153,15 +4147,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -4184,15 +4171,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, @@ -56664,14 +56644,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, @@ -56790,15 +56764,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -56821,15 +56788,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0) && (fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { - zend_function *deopt = zend_get_deoptimized_function(fbc); - if (deopt) { - fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); - } - used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); - } + if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { + zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, From 2fe0a979e1a181d1f0eebdfd671469ec275089e7 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 02:58:15 +0100 Subject: [PATCH 08/15] wip --- Zend/Optimizer/zend_call_graph.c | 4 +- Zend/Optimizer/zend_optimizer.c | 53 ++-------------------- Zend/Optimizer/zend_optimizer.h | 3 -- Zend/zend_bitset.h | 10 ++++ Zend/zend_compile.c | 78 +++++++++++++++++++++++++------- Zend/zend_compile.h | 10 ++++ Zend/zend_execute.c | 22 ++++++--- Zend/zend_execute_API.c | 7 +-- Zend/zend_globals.h | 12 ++--- Zend/zend_opcode.c | 65 ++++++++++++++++++++++++++ Zend/zend_vm_def.h | 14 +++--- Zend/zend_vm_execute.h | 28 ++++++------ ext/opcache/jit/zend_jit.c | 2 +- main/main.c | 3 ++ 14 files changed, 202 insertions(+), 109 deletions(-) diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c index 884b481aceb8..1b3eb537167b 100644 --- a/Zend/Optimizer/zend_call_graph.c +++ b/Zend/Optimizer/zend_call_graph.c @@ -238,12 +238,12 @@ static void zend_sort_op_arrays(zend_call_graph *call_graph) ZEND_API void zend_build_call_graph(zend_arena **arena, zend_script *script, zend_call_graph *call_graph) /* {{{ */ { call_graph->op_arrays_count = 0; - zend_foreach_op_array(script, zend_op_array_calc, call_graph); + zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, zend_op_array_calc, call_graph); call_graph->op_arrays = (zend_op_array**)zend_arena_calloc(arena, call_graph->op_arrays_count, sizeof(zend_op_array*)); call_graph->func_infos = (zend_func_info*)zend_arena_calloc(arena, call_graph->op_arrays_count, sizeof(zend_func_info)); call_graph->op_arrays_count = 0; - zend_foreach_op_array(script, zend_op_array_collect, call_graph); + zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, zend_op_array_collect, call_graph); } /* }}} */ diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index f8cbefdaaf2b..14db2bb29c1f 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1526,53 +1526,6 @@ static bool needs_live_range(const zend_op_array *op_array, const zend_op *def_o return (type & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) != 0; } -static void zend_foreach_op_array_helper( - zend_op_array *op_array, zend_op_array_func_t func, void *context) { - func(op_array, context); - for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { - zend_foreach_op_array_helper(op_array->dynamic_func_defs[i], func, context); - } -} - -void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context) -{ - zval *zv; - zend_op_array *op_array; - - zend_foreach_op_array_helper(&script->main_op_array, func, context); - - ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { - zend_foreach_op_array_helper(op_array, func, context); - } ZEND_HASH_FOREACH_END(); - - ZEND_HASH_MAP_FOREACH_VAL(&script->class_table, zv) { - if (Z_TYPE_P(zv) == IS_ALIAS_PTR) { - continue; - } - const zend_class_entry *ce = Z_CE_P(zv); - ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { - if (op_array->scope == ce - && op_array->type == ZEND_USER_FUNCTION - && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) - && !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { - zend_foreach_op_array_helper(op_array, func, context); - } - } ZEND_HASH_FOREACH_END(); - - zend_property_info *property; - ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, property) { - zend_function **hooks = property->hooks; - if (property->ce == ce && property->hooks) { - for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { - const zend_function *hook = hooks[i]; - if (hook && hook->common.scope == ce && !(hooks[i]->op_array.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - zend_foreach_op_array_helper(&hooks[i]->op_array, func, context); - } - } - } - } ZEND_HASH_FOREACH_END(); - } ZEND_HASH_FOREACH_END(); -} static void step_optimize_op_array(zend_op_array *op_array, void *context) { zend_optimize_op_array(op_array, (zend_optimizer_ctx *) context); @@ -1713,10 +1666,10 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l ZEND_SET_FUNC_INFO(call_graph.op_arrays[i], NULL); } } else { - zend_foreach_op_array(script, step_optimize_op_array, &ctx); + zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_optimize_op_array, &ctx); if (ZEND_OPTIMIZER_PASS_12 & optimization_level) { - zend_foreach_op_array(script, step_adjust_fcall_stack_size, &ctx); + zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_adjust_fcall_stack_size, &ctx); } } @@ -1751,7 +1704,7 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l if ((debug_level & ZEND_DUMP_AFTER_OPTIMIZER) && (ZEND_OPTIMIZER_PASS_7 & optimization_level)) { - zend_foreach_op_array(script, step_dump_after_optimizer, NULL); + zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_dump_after_optimizer, NULL); } if (ctx.constants) { diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index 43a0f60a2321..2453375f87a8 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -99,9 +99,6 @@ ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); zend_result zend_optimizer_shutdown(void); -typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); -void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context); - END_EXTERN_C() #endif diff --git a/Zend/zend_bitset.h b/Zend/zend_bitset.h index d990b6a2a871..7b6833ecb110 100644 --- a/Zend/zend_bitset.h +++ b/Zend/zend_bitset.h @@ -296,4 +296,14 @@ static inline int zend_bitset_pop_first(zend_bitset set, uint32_t len) { return i; } +static inline bool zend_bitset_has_intersection(zend_bitset set1, zend_bitset set2, uint32_t len) +{ + for (uint32_t i = 0; i < len; i++) { + if (set1[i] & set2[i]) { + return 1; + } + } + return 0; +} + #endif /* _ZEND_BITSET_H_ */ diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index bee7081670f2..8089664ec2f5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -39,6 +39,7 @@ #include "zend_call_stack.h" #include "zend_frameless_function.h" #include "zend_property_hooks.h" +#include "zend_bitset.h" #define SET_NODE(target, src) do { \ target ## _type = (src)->op_type; \ @@ -1279,8 +1280,39 @@ ZEND_API void function_add_ref(zend_function *function) /* {{{ */ } /* }}} */ +/* Record that the active op_array assumes the internal function at the given + * arData index is the global (non-shadowed) version. */ +static void zend_record_global_func_assumption(uint32_t ardata_index) +{ + zend_op_array *op_array = CG(active_op_array); + uint32_t len = zend_bitset_len(CG(num_global_internal_funcs)); + if (!op_array->global_func_assumptions) { + op_array->global_func_assumptions = emalloc(len * sizeof(zend_ulong)); + memset(op_array->global_func_assumptions, 0, len * sizeof(zend_ulong)); + } + zend_bitset_incl(op_array->global_func_assumptions, ardata_index); + op_array->fn_flags2 |= ZEND_ACC2_ASSUMPTIONS; +} + +/* Clear all runtime caches so that function lookups are re-evaluated. + * Called when a new namespace shadow is declared. */ +static void zend_clear_op_array_runtime_cache(zend_op_array *op_array, void *context) +{ + (void)context; + void **cache = ZEND_MAP_PTR_GET(op_array->run_time_cache); + if (cache) { + memset(cache, 0, op_array->cache_size); + } +} + +static void zend_clear_all_runtime_caches(void) +{ + zend_foreach_op_array(NULL, EG(function_table), EG(class_table), + zend_clear_op_array_runtime_cache, NULL); +} + /* Check if a namespaced function shadows a global function. - * If so, bump the generation counter to invalidate NS global assumptions. */ + * If so, set the corresponding bit in the global shadow bitmap. */ static void zend_check_ns_function_shadow(const zend_string *lcname) { const char *backslash = zend_memrchr(ZSTR_VAL(lcname), '\\', ZSTR_LEN(lcname)); @@ -1290,23 +1322,31 @@ static void zend_check_ns_function_shadow(const zend_string *lcname) const char *unqualified = backslash + 1; size_t unqualified_len = ZSTR_LEN(lcname) - (unqualified - ZSTR_VAL(lcname)); zend_string *global_name = zend_string_init(unqualified, unqualified_len, 0); - if (zend_hash_exists(CG(function_table), global_name)) { - EG(num_shadowed_global_funcs)++; + const zval *global_fbc_zv = zend_hash_find(EG(function_table), global_name); + if (global_fbc_zv) { + const zend_function *global_fbc = Z_PTR_P(global_fbc_zv); + if (!ZEND_USER_CODE(global_fbc->type)) { + const Bucket *bucket = (const Bucket *)((uintptr_t)global_fbc_zv - XtOffsetOf(Bucket, val)); + uint32_t ardata_index = bucket - EG(function_table)->arData; + if (ardata_index < CG(num_global_internal_funcs)) { + zend_bitset_incl(EG(shadowed_global_funcs), ardata_index); + zend_clear_all_runtime_caches(); + } + } } zend_string_release(global_name); } /* Recompile a function without NS global assumptions. - * Returns the deoptimized function or NULL on failure. - * The result is cached in EG(deoptimized_funcs). */ + * Returns the deoptimized op_array or NULL on failure. + * The result is cached on func->op_array.deoptimized. */ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) { ZEND_ASSERT(func->type == ZEND_USER_FUNCTION); /* Check cache first. */ - zend_function *deopt = zend_hash_find_ptr(&EG(deoptimized_funcs), func->common.function_name); - if (deopt) { - return deopt; + if (func->op_array.deoptimized) { + return (zend_function *)func->op_array.deoptimized; } /* Temporarily remove user functions from the same file to avoid @@ -1345,7 +1385,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) CG(compiler_options) = orig_compiler_options; /* Find the deoptimized function by name. */ - deopt = NULL; + zend_function *deopt = NULL; if (main_op_array && func->op_array.function_name) { zend_string *lc_name = zend_string_tolower(func->op_array.function_name); deopt = zend_hash_find_ptr(EG(function_table), lc_name); @@ -1365,9 +1405,9 @@ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) efree(main_op_array); } - /* Cache the result. */ + /* Cache the result on the original op_array. */ if (deopt) { - zend_hash_add_new_ptr(&EG(deoptimized_funcs), deopt->common.function_name, deopt); + ((zend_function *)func)->op_array.deoptimized = &deopt->op_array; } return deopt; @@ -4921,11 +4961,14 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast zend_string *lc_func_name = Z_STR_P(CT_CONSTANT_EX(CG(active_op_array), name_constants + 2)); if (!zend_hash_exists(CG(function_table), lc_ns_name) && !zend_have_seen_symbol(lc_ns_name, ZEND_SYMBOL_FUNCTION)) { - const zend_function *frameless_function = zend_hash_find_ptr(CG(function_table), lc_func_name); - if (frameless_function) { + const zval *frameless_fbc_zv = zend_hash_find(CG(function_table), lc_func_name); + if (frameless_fbc_zv) { + const zend_function *frameless_function = Z_PTR_P(frameless_fbc_zv); const zend_frameless_function_info *info = find_frameless_function_info(zend_ast_get_list(args_ast), frameless_function, type); if (info) { - CG(active_op_array)->fn_flags2 |= ZEND_ACC2_ASSUMPTIONS; + const Bucket *fbc_bucket = (const Bucket *)((uintptr_t)frameless_fbc_zv - XtOffsetOf(Bucket, val)); + uint32_t ardata_index = fbc_bucket - CG(function_table)->arData; + zend_record_global_func_assumption(ardata_index); zend_compile_frameless_icall_ex(result, zend_ast_get_list(args_ast), frameless_function, info, type); return; } @@ -5505,7 +5548,8 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) zend_string *orig_name = zend_ast_get_str(name_ast); zend_string *lc_orig_name = zend_string_tolower(orig_name); zend_string *lc_ns_name = zend_string_tolower(Z_STR(name_node.u.constant)); - const zend_function *global_fbc = zend_hash_find_ptr(CG(function_table), lc_orig_name); + const zval *global_fbc_zv = zend_hash_find(CG(function_table), lc_orig_name); + const zend_function *global_fbc = global_fbc_zv ? Z_PTR_P(global_fbc_zv) : NULL; bool ns_func_exists = zend_hash_exists(CG(function_table), lc_ns_name) || zend_have_seen_symbol(lc_ns_name, ZEND_SYMBOL_FUNCTION); zend_string_release(lc_ns_name); @@ -5519,10 +5563,12 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) /* Assume the unqualified call resolves to the global function. * Replace NS-qualified name with unqualified name and fall * through to the fully-qualified compilation path. */ + const Bucket *fbc_bucket = (const Bucket *)((uintptr_t)global_fbc_zv - XtOffsetOf(Bucket, val)); + uint32_t ardata_index = fbc_bucket - CG(function_table)->arData; zval_ptr_dtor(&name_node.u.constant); ZVAL_STR_COPY(&name_node.u.constant, orig_name); zend_string_release(lc_orig_name); - CG(active_op_array)->fn_flags2 |= ZEND_ACC2_ASSUMPTIONS; + zend_record_global_func_assumption(ardata_index); goto resolve_as_global; } diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index ace214f2fa6f..4984c1ea10aa 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -546,6 +546,7 @@ struct _zend_op_array { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ + zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ /* END of common elements */ uint32_t cache_size; /* number of run_time_cache_slots * sizeof(void*) */ @@ -576,6 +577,8 @@ struct _zend_op_array { * referenced by index from opcodes. */ zend_op_array **dynamic_func_defs; + struct _zend_op_array *deoptimized; /* recompiled version without NS global assumptions */ + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; @@ -606,6 +609,7 @@ typedef struct _zend_internal_function { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ + zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ /* END of common elements */ zif_handler handler; @@ -636,6 +640,7 @@ union _zend_function { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ + zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ } common; zend_op_array op_array; @@ -1330,4 +1335,9 @@ bool zend_try_ct_eval_cast(zval *result, uint32_t type, zval *op1); bool zend_op_may_elide_result(uint8_t opcode); +typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); +void zend_foreach_op_array( + zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, + zend_op_array_func_t func, void *context); + #endif /* ZEND_COMPILE_H */ diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 491eb82d93b2..501f19be4f3e 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -25,6 +25,7 @@ #include "zend.h" #include "zend_compile.h" +#include "zend_bitset.h" #include "zend_execute.h" #include "zend_API.h" #include "zend_ptr_stack.h" @@ -154,6 +155,7 @@ ZEND_API const zend_internal_function zend_pass_function = { 0, /* T */ 0, /* fn_flags2 */ NULL, /* prop_info */ + NULL, /* global_func_assumptions */ ZEND_FN(pass), /* handler */ NULL, /* module */ NULL, /* frameless_function_infos */ @@ -5897,21 +5899,27 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint /* This callback disables optimization of "vm_stack_data" variable in VM */ ZEND_API void (ZEND_FASTCALL *zend_touch_vm_stack_data)(void *vm_stack_data) = NULL; -static zend_never_inline ZEND_COLD void zend_fetch_deoptimized_func(zend_function **fbc_ptr, uint32_t *used_stack OPLINE_DC) +static zend_never_inline ZEND_COLD void zend_maybe_deoptimize_func(zend_function **fbc_ptr, uint32_t *used_stack OPLINE_DC) { zend_function *fbc = *fbc_ptr; - if (!(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_bitset assumptions = fbc->op_array.global_func_assumptions; + if (!assumptions || !zend_bitset_has_intersection( + assumptions, EG(shadowed_global_funcs), EG(shadowed_global_funcs_len))) { return; } - zend_function *deopt = zend_get_deoptimized_function(fbc); + zend_op_array *deopt = fbc->op_array.deoptimized; if (!deopt) { - return; + deopt = (zend_op_array *)zend_get_deoptimized_function(fbc); + if (!deopt) { + return; + } + fbc->op_array.deoptimized = deopt; } - *fbc_ptr = fbc = deopt; - if (UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); + *fbc_ptr = fbc = (zend_function *)deopt; + if (UNEXPECTED(!RUN_TIME_CACHE(deopt))) { + init_func_run_time_cache(deopt); } if (used_stack) { *used_stack = zend_vm_calc_used_stack(opline->extended_value, fbc); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 31c772e64ecf..9d7e360c4b8a 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -24,6 +24,7 @@ #include "zend.h" #include "zend_compile.h" #include "zend_execute.h" +#include "zend_bitset.h" #include "zend_API.h" #include "zend_stack.h" #include "zend_constants.h" @@ -204,8 +205,8 @@ void init_executor(void) /* {{{ */ zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); - EG(num_shadowed_global_funcs) = 0; - zend_hash_init(&EG(deoptimized_funcs), 0, NULL, ZEND_FUNCTION_DTOR, 0); + EG(shadowed_global_funcs_len) = zend_bitset_len(CG(num_global_internal_funcs)); + EG(shadowed_global_funcs) = ecalloc(EG(shadowed_global_funcs_len), sizeof(zend_ulong)); EG(active) = 1; } @@ -519,7 +520,7 @@ void shutdown_executor(void) /* {{{ */ } zend_hash_destroy(&EG(callable_convert_cache)); - zend_hash_destroy(&EG(deoptimized_funcs)); + efree(EG(shadowed_global_funcs)); } #if ZEND_DEBUG diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index c822263e7d7d..e071ff9e60bb 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -95,6 +95,8 @@ struct _zend_compiler_globals { HashTable *function_table; /* function symbol table */ HashTable *class_table; /* class table */ + uint32_t num_global_internal_funcs; /* count of internal funcs at startup, determines bitmap size */ + HashTable *auto_globals; /* Refer to zend_yytnamerr() in zend_language_parser.y for meaning of values */ @@ -321,12 +323,10 @@ struct _zend_executor_globals { HashTable callable_convert_cache; - /* Bumped when a namespaced function shadowing a global function is declared. - * Used to invalidate NS global function assumptions made during compilation. */ - uint32_t num_shadowed_global_funcs; - - /* Cache of deoptimized op_arrays, keyed by function name. */ - HashTable deoptimized_funcs; + /* Bitmap of internal functions (by arData index) that have been shadowed + * by a namespaced function. Compared against per-op_array assumption bitmaps. */ + zend_ulong *shadowed_global_funcs; + uint32_t shadowed_global_funcs_len; /* length in zend_ulong words */ void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 24b480ad71e6..1f0dd3d41451 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -85,6 +85,8 @@ void init_op_array(zend_op_array *op_array, zend_function_type type, int initial op_array->fn_flags = 0; op_array->fn_flags2 = 0; + op_array->global_func_assumptions = NULL; + op_array->deoptimized = NULL; op_array->last_literal = 0; op_array->literals = NULL; @@ -661,6 +663,13 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) } efree(op_array->dynamic_func_defs); } + if (op_array->global_func_assumptions) { + efree(op_array->global_func_assumptions); + } + if (op_array->deoptimized) { + destroy_op_array(op_array->deoptimized); + efree(op_array->deoptimized); + } } static void zend_update_extended_stmts(zend_op_array *op_array) @@ -1319,3 +1328,59 @@ ZEND_API binary_op_type get_binary_op(int opcode) return (binary_op_type) NULL; } } + +static void zend_foreach_op_array_helper( + zend_op_array *op_array, zend_op_array_func_t func, void *context) { + func(op_array, context); + for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { + zend_foreach_op_array_helper(op_array->dynamic_func_defs[i], func, context); + } + if (op_array->deoptimized) { + zend_foreach_op_array_helper(op_array->deoptimized, func, context); + } +} + +void zend_foreach_op_array( + zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, + zend_op_array_func_t func, void *context) +{ + if (main_op_array) { + zend_foreach_op_array_helper(main_op_array, func, context); + } + + zval *zv; + zend_op_array *op_array; + + ZEND_HASH_MAP_FOREACH_PTR(function_table, op_array) { + if (op_array->type == ZEND_USER_FUNCTION) { + zend_foreach_op_array_helper(op_array, func, context); + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_VAL(class_table, zv) { + if (Z_TYPE_P(zv) == IS_ALIAS_PTR) { + continue; + } + const zend_class_entry *ce = Z_CE_P(zv); + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { + if (op_array->scope == ce + && op_array->type == ZEND_USER_FUNCTION + && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) + && !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zend_foreach_op_array_helper(op_array, func, context); + } + } ZEND_HASH_FOREACH_END(); + + zend_property_info *property; + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, property) { + if (property->ce == ce && property->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + zend_function *hook = property->hooks[i]; + if (hook && hook->common.scope == ce && !(hook->op_array.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zend_foreach_op_array_helper(&hook->op_array, func, context); + } + } + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); +} diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 986f14207a83..1a7e5f4261f3 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3910,11 +3910,11 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); + } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); - } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); @@ -4103,8 +4103,8 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT) } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -4127,8 +4127,8 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2 CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index b090d1c338b2..da84ff3a29b5 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4025,11 +4025,11 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); + } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); - } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); @@ -4147,8 +4147,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -4171,8 +4171,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, @@ -56642,11 +56642,11 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); + } CACHE_PTR(opline->result.num, fbc); } - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, NULL OPLINE_CC); - } call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); @@ -56764,8 +56764,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -56788,8 +56788,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(EG(num_shadowed_global_funcs) > 0)) { - zend_fetch_deoptimized_func(&fbc, &used_stack OPLINE_CC); + if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( used_stack, ZEND_CALL_NESTED_FUNCTION, diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 3ffb669e8474..5bfc91a78eb8 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3973,7 +3973,7 @@ static void zend_jit_restart_preloaded_op_array(zend_op_array *op_array, void *c static void zend_jit_restart_preloaded_script(zend_persistent_script *script) { - zend_foreach_op_array(&script->script, zend_jit_restart_preloaded_op_array, NULL); + zend_foreach_op_array(&script->script.main_op_array, &script->script.function_table, &script->script.class_table, zend_jit_restart_preloaded_op_array, NULL); } void zend_jit_restart(void) diff --git a/main/main.c b/main/main.c index bbd2d18ba187..3c4969590a12 100644 --- a/main/main.c +++ b/main/main.c @@ -2373,6 +2373,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi } } + /* Record the number of internal functions for deoptimization bitmap sizing. */ + CG(num_global_internal_funcs) = CG(function_table)->nNumUsed; + /* disable certain functions as requested by php.ini */ zend_disable_functions(INI_STR("disable_functions")); From ef9417284f4b5d31e2abf349571629fd108149d1 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 12:34:48 +0100 Subject: [PATCH 09/15] Adjustments --- Zend/zend_compile.c | 7 ++++++- Zend/zend_compile.h | 4 +--- Zend/zend_execute.c | 5 ++--- Zend/zend_execute_API.c | 9 ++++++--- Zend/zend_opcode.c | 2 +- Zend/zend_vm_def.h | 6 +++--- Zend/zend_vm_execute.h | 12 ++++++------ 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 8089664ec2f5..2ba632351ef5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1329,6 +1329,11 @@ static void zend_check_ns_function_shadow(const zend_string *lcname) const Bucket *bucket = (const Bucket *)((uintptr_t)global_fbc_zv - XtOffsetOf(Bucket, val)); uint32_t ardata_index = bucket - EG(function_table)->arData; if (ardata_index < CG(num_global_internal_funcs)) { + if (!EG(shadowed_global_funcs)) { + EG(shadowed_global_funcs_len) = zend_bitset_len(CG(num_global_internal_funcs)); + // FIXME: Arena allocation sufficient? + EG(shadowed_global_funcs) = ecalloc(EG(shadowed_global_funcs_len), sizeof(zend_ulong)); + } zend_bitset_incl(EG(shadowed_global_funcs), ardata_index); zend_clear_all_runtime_caches(); } @@ -5549,7 +5554,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) zend_string *lc_orig_name = zend_string_tolower(orig_name); zend_string *lc_ns_name = zend_string_tolower(Z_STR(name_node.u.constant)); const zval *global_fbc_zv = zend_hash_find(CG(function_table), lc_orig_name); - const zend_function *global_fbc = global_fbc_zv ? Z_PTR_P(global_fbc_zv) : NULL; + const zend_function *global_fbc = global_fbc_zv ? Z_PTR_P(global_fbc_zv) : NULL; bool ns_func_exists = zend_hash_exists(CG(function_table), lc_ns_name) || zend_have_seen_symbol(lc_ns_name, ZEND_SYMBOL_FUNCTION); zend_string_release(lc_ns_name); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 4984c1ea10aa..dc15d934d65c 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -546,7 +546,6 @@ struct _zend_op_array { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ - zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ /* END of common elements */ uint32_t cache_size; /* number of run_time_cache_slots * sizeof(void*) */ @@ -577,6 +576,7 @@ struct _zend_op_array { * referenced by index from opcodes. */ zend_op_array **dynamic_func_defs; + zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ struct _zend_op_array *deoptimized; /* recompiled version without NS global assumptions */ void *reserved[ZEND_MAX_RESERVED_RESOURCES]; @@ -609,7 +609,6 @@ typedef struct _zend_internal_function { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ - zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ /* END of common elements */ zif_handler handler; @@ -640,7 +639,6 @@ union _zend_function { uint32_t T; /* number of temporary variables */ uint32_t fn_flags2; const zend_property_info *prop_info; /* The corresponding prop_info if this is a hook. */ - zend_ulong *global_func_assumptions; /* bitset: which internal funcs are assumed global */ } common; zend_op_array op_array; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 501f19be4f3e..c5edd27c52a0 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -155,7 +155,6 @@ ZEND_API const zend_internal_function zend_pass_function = { 0, /* T */ 0, /* fn_flags2 */ NULL, /* prop_info */ - NULL, /* global_func_assumptions */ ZEND_FN(pass), /* handler */ NULL, /* module */ NULL, /* frameless_function_infos */ @@ -5903,8 +5902,8 @@ static zend_never_inline ZEND_COLD void zend_maybe_deoptimize_func(zend_function { zend_function *fbc = *fbc_ptr; zend_bitset assumptions = fbc->op_array.global_func_assumptions; - if (!assumptions || !zend_bitset_has_intersection( - assumptions, EG(shadowed_global_funcs), EG(shadowed_global_funcs_len))) { + ZEND_ASSERT(assumptions && EG(shadowed_global_funcs)); + if (!zend_bitset_has_intersection(assumptions, EG(shadowed_global_funcs), EG(shadowed_global_funcs_len))) { return; } diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 9d7e360c4b8a..1f4008cb0c81 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -205,8 +205,8 @@ void init_executor(void) /* {{{ */ zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); - EG(shadowed_global_funcs_len) = zend_bitset_len(CG(num_global_internal_funcs)); - EG(shadowed_global_funcs) = ecalloc(EG(shadowed_global_funcs_len), sizeof(zend_ulong)); + EG(shadowed_global_funcs_len) = 0; + EG(shadowed_global_funcs) = NULL; EG(active) = 1; } @@ -520,7 +520,10 @@ void shutdown_executor(void) /* {{{ */ } zend_hash_destroy(&EG(callable_convert_cache)); - efree(EG(shadowed_global_funcs)); + + if (EG(shadowed_global_funcs)) { + efree(EG(shadowed_global_funcs)); + } } #if ZEND_DEBUG diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 1f0dd3d41451..24258437acba 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -668,7 +668,7 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) } if (op_array->deoptimized) { destroy_op_array(op_array->deoptimized); - efree(op_array->deoptimized); + /* op_array->deoptimized is allocated on CG(arena), not individually emalloc'd */ } } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 1a7e5f4261f3..a10fd1bcd98f 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3910,7 +3910,7 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); } CACHE_PTR(opline->result.num, fbc); @@ -4103,7 +4103,7 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT) } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } @@ -4127,7 +4127,7 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2 CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index da84ff3a29b5..94ec00bdacd1 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4025,7 +4025,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); } CACHE_PTR(opline->result.num, fbc); @@ -4147,7 +4147,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } @@ -4171,7 +4171,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( @@ -56642,7 +56642,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, NULL OPLINE_CC); } CACHE_PTR(opline->result.num, fbc); @@ -56764,7 +56764,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } @@ -56788,7 +56788,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F CACHE_PTR(opline->result.num, fbc); } uint32_t used_stack = opline->op1.num; - if (UNEXPECTED(fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS)) { + if (UNEXPECTED(EG(shadowed_global_funcs)) && fbc->common.fn_flags2 & ZEND_ACC2_ASSUMPTIONS) { zend_maybe_deoptimize_func(&fbc, &used_stack OPLINE_CC); } call = _zend_vm_stack_push_call_frame_ex( From d24faa364b0423b26667f0d57e66f2b3a3cc0eb9 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 12:51:05 +0100 Subject: [PATCH 10/15] Revert foreach_op_array changes --- Zend/Optimizer/zend_call_graph.c | 4 ++-- Zend/Optimizer/zend_optimizer.c | 10 +++++++--- Zend/Optimizer/zend_optimizer.h | 3 +++ Zend/zend_compile.c | 4 ++-- Zend/zend_compile.h | 4 +--- Zend/zend_opcode.c | 14 +++++--------- ext/opcache/jit/zend_jit.c | 2 +- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c index 1b3eb537167b..884b481aceb8 100644 --- a/Zend/Optimizer/zend_call_graph.c +++ b/Zend/Optimizer/zend_call_graph.c @@ -238,12 +238,12 @@ static void zend_sort_op_arrays(zend_call_graph *call_graph) ZEND_API void zend_build_call_graph(zend_arena **arena, zend_script *script, zend_call_graph *call_graph) /* {{{ */ { call_graph->op_arrays_count = 0; - zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, zend_op_array_calc, call_graph); + zend_foreach_op_array(script, zend_op_array_calc, call_graph); call_graph->op_arrays = (zend_op_array**)zend_arena_calloc(arena, call_graph->op_arrays_count, sizeof(zend_op_array*)); call_graph->func_infos = (zend_func_info*)zend_arena_calloc(arena, call_graph->op_arrays_count, sizeof(zend_func_info)); call_graph->op_arrays_count = 0; - zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, zend_op_array_collect, call_graph); + zend_foreach_op_array(script, zend_op_array_collect, call_graph); } /* }}} */ diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index 14db2bb29c1f..a1c758554ef6 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1526,6 +1526,10 @@ static bool needs_live_range(const zend_op_array *op_array, const zend_op *def_o return (type & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) != 0; } +void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context) +{ + zend_foreach_op_array_ex(&script->main_op_array, &script->function_table, &script->class_table, func, context); +} static void step_optimize_op_array(zend_op_array *op_array, void *context) { zend_optimize_op_array(op_array, (zend_optimizer_ctx *) context); @@ -1666,10 +1670,10 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l ZEND_SET_FUNC_INFO(call_graph.op_arrays[i], NULL); } } else { - zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_optimize_op_array, &ctx); + zend_foreach_op_array(script, step_optimize_op_array, &ctx); if (ZEND_OPTIMIZER_PASS_12 & optimization_level) { - zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_adjust_fcall_stack_size, &ctx); + zend_foreach_op_array(script, step_adjust_fcall_stack_size, &ctx); } } @@ -1704,7 +1708,7 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l if ((debug_level & ZEND_DUMP_AFTER_OPTIMIZER) && (ZEND_OPTIMIZER_PASS_7 & optimization_level)) { - zend_foreach_op_array(&script->main_op_array, &script->function_table, &script->class_table, step_dump_after_optimizer, NULL); + zend_foreach_op_array(script, step_dump_after_optimizer, NULL); } if (ctx.constants) { diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index 2453375f87a8..43a0f60a2321 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -99,6 +99,9 @@ ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); zend_result zend_optimizer_shutdown(void); +typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); +void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context); + END_EXTERN_C() #endif diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 2ba632351ef5..e1f94d583e78 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1307,8 +1307,8 @@ static void zend_clear_op_array_runtime_cache(zend_op_array *op_array, void *con static void zend_clear_all_runtime_caches(void) { - zend_foreach_op_array(NULL, EG(function_table), EG(class_table), - zend_clear_op_array_runtime_cache, NULL); + // FIXME: Skip internal functions and classes? + zend_foreach_op_array_ex(NULL, EG(function_table), EG(class_table), zend_clear_op_array_runtime_cache, NULL); } /* Check if a namespaced function shadows a global function. diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index dc15d934d65c..3daefa4bd9a6 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1334,8 +1334,6 @@ bool zend_try_ct_eval_cast(zval *result, uint32_t type, zval *op1); bool zend_op_may_elide_result(uint8_t opcode); typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); -void zend_foreach_op_array( - zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, - zend_op_array_func_t func, void *context); +void zend_foreach_op_array_ex(zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, zend_op_array_func_t func, void *context); #endif /* ZEND_COMPILE_H */ diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 24258437acba..124fd25c13e7 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -1340,29 +1340,25 @@ static void zend_foreach_op_array_helper( } } -void zend_foreach_op_array( - zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, - zend_op_array_func_t func, void *context) +void zend_foreach_op_array_ex(zend_op_array *main_op_array, HashTable *function_table, HashTable *class_table, zend_op_array_func_t func, void *context) { if (main_op_array) { zend_foreach_op_array_helper(main_op_array, func, context); } - zval *zv; - zend_op_array *op_array; - - ZEND_HASH_MAP_FOREACH_PTR(function_table, op_array) { + ZEND_HASH_MAP_FOREACH_PTR(function_table, zend_op_array *op_array) { if (op_array->type == ZEND_USER_FUNCTION) { zend_foreach_op_array_helper(op_array, func, context); } } ZEND_HASH_FOREACH_END(); - ZEND_HASH_MAP_FOREACH_VAL(class_table, zv) { + ZEND_HASH_MAP_FOREACH_VAL(class_table, zval *zv) { if (Z_TYPE_P(zv) == IS_ALIAS_PTR) { continue; } + const zend_class_entry *ce = Z_CE_P(zv); - ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, zend_op_array *op_array) { if (op_array->scope == ce && op_array->type == ZEND_USER_FUNCTION && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 5bfc91a78eb8..3ffb669e8474 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3973,7 +3973,7 @@ static void zend_jit_restart_preloaded_op_array(zend_op_array *op_array, void *c static void zend_jit_restart_preloaded_script(zend_persistent_script *script) { - zend_foreach_op_array(&script->script.main_op_array, &script->script.function_table, &script->script.class_table, zend_jit_restart_preloaded_op_array, NULL); + zend_foreach_op_array(&script->script, zend_jit_restart_preloaded_op_array, NULL); } void zend_jit_restart(void) From 6192b481deeb0a9e318b2e8ceea8f404e8249b23 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 13:07:34 +0100 Subject: [PATCH 11/15] Fix opcache --- ext/opcache/zend_persist.c | 8 ++++++++ ext/opcache/zend_persist_calc.c | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 9bc2496837ce..a1eda807d4f5 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -29,6 +29,7 @@ #include "zend_operators.h" #include "zend_interfaces.h" #include "zend_attributes.h" +#include "zend_bitset.h" #ifdef HAVE_JIT # include "Optimizer/zend_func_info.h" @@ -699,6 +700,13 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc } } + if (op_array->global_func_assumptions) { + op_array->global_func_assumptions = zend_shared_memdup_put_free(op_array->global_func_assumptions, sizeof(zend_ulong) * zend_bitset_len(CG(num_global_internal_funcs))); + } + + /* Function has never been run, deoptimized must still be NULL. */ + ZEND_ASSERT(!op_array->deoptimized); + ZCG(mem) = (void*)((char*)ZCG(mem) + ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist(op_array, ZCG(mem)))); } diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 0b0ff51d0d4d..2b434a2ddc92 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -27,6 +27,7 @@ #include "zend_operators.h" #include "zend_attributes.h" #include "zend_constants.h" +#include "zend_bitset.h" #define ADD_DUP_SIZE(m,s) ZCG(current_persistent_script)->size += zend_shared_memdup_size((void*)m, s) #define ADD_SIZE(m) ZCG(current_persistent_script)->size += ZEND_ALIGNED_SIZE(m) @@ -343,6 +344,13 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) } } + if (op_array->global_func_assumptions) { + ADD_SIZE(sizeof(zend_ulong) * zend_bitset_len(CG(num_global_internal_funcs))); + } + + /* Function has never been run, deoptimized must still be NULL. */ + ZEND_ASSERT(!op_array->deoptimized); + ADD_SIZE(ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist_calc(op_array))); } From 625c2501db82b12b9f249051b470660e86b4234b Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 13:50:52 +0100 Subject: [PATCH 12/15] Avoid const UB --- Zend/zend_compile.c | 4 ++-- Zend/zend_compile.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e1f94d583e78..55db55d5bdb0 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1345,7 +1345,7 @@ static void zend_check_ns_function_shadow(const zend_string *lcname) /* Recompile a function without NS global assumptions. * Returns the deoptimized op_array or NULL on failure. * The result is cached on func->op_array.deoptimized. */ -ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) +ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func) { ZEND_ASSERT(func->type == ZEND_USER_FUNCTION); @@ -1412,7 +1412,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func) /* Cache the result on the original op_array. */ if (deopt) { - ((zend_function *)func)->op_array.deoptimized = &deopt->op_array; + func->op_array.deoptimized = &deopt->op_array; } return deopt; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 3daefa4bd9a6..774266e29991 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -968,7 +968,7 @@ ZEND_API zend_result zend_execute_script(int type, zval *retval, zend_file_handl ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle); ZEND_API void init_op_array(zend_op_array *op_array, zend_function_type type, int initial_ops_size); ZEND_API void destroy_op_array(zend_op_array *op_array); -ZEND_API zend_function *zend_get_deoptimized_function(const zend_function *func); +ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func); ZEND_API void zend_destroy_static_vars(zend_op_array *op_array); ZEND_API void zend_destroy_file_handle(zend_file_handle *file_handle); ZEND_API void zend_cleanup_mutable_class_data(zend_class_entry *ce); From 67a41179d89e574f079ae5d224971191cf332ef0 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 15:07:55 +0100 Subject: [PATCH 13/15] Fix shm protection --- Zend/zend_compile.c | 6 +++++- Zend/zend_execute.c | 1 - Zend/zend_execute.h | 1 + Zend/zend_execute_API.c | 1 + ext/opcache/ZendAccelerator.c | 8 ++++++++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 55db55d5bdb0..26ca574178cf 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1412,7 +1412,11 @@ ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func) /* Cache the result on the original op_array. */ if (deopt) { - func->op_array.deoptimized = &deopt->op_array; + if (!zend_store_deoptimized_op_array) { + func->op_array.deoptimized = &deopt->op_array; + } else { + zend_store_deoptimized_op_array(&func->op_array, &deopt->op_array); + } } return deopt; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index c5edd27c52a0..6ac4750c08b4 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5913,7 +5913,6 @@ static zend_never_inline ZEND_COLD void zend_maybe_deoptimize_func(zend_function if (!deopt) { return; } - fbc->op_array.deoptimized = deopt; } *fbc_ptr = fbc = (zend_function *)deopt; diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 89a9e79143a8..09d18d4020d8 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -36,6 +36,7 @@ ZEND_API extern void (*zend_execute_internal)(zend_execute_data *execute_data, z /* The lc_name may be stack allocated! */ ZEND_API extern zend_class_entry *(*zend_autoload)(zend_string *name, zend_string *lc_name); +ZEND_API extern void (*zend_store_deoptimized_op_array)(zend_op_array *optimized, zend_op_array *deoptimized); void init_executor(void); void shutdown_executor(void); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 1f4008cb0c81..dd16a58df214 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -53,6 +53,7 @@ ZEND_API void (*zend_execute_ex)(zend_execute_data *execute_data); ZEND_API void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value); ZEND_API zend_class_entry *(*zend_autoload)(zend_string *name, zend_string *lc_name); +ZEND_API void (*zend_store_deoptimized_op_array)(zend_op_array *optimized, zend_op_array *deoptimized); #ifdef ZEND_WIN32 ZEND_TLS HANDLE tq_timer = NULL; diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index f4134212bc4e..e82c8120298c 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -3249,6 +3249,13 @@ static int accel_startup(zend_extension *extension) return SUCCESS; } +static void store_deoptimized_op_array(zend_op_array *optimized, zend_op_array *deoptimized) +{ + SHM_UNPROTECT(); + optimized->deoptimized = deoptimized; + SHM_PROTECT(); +} + static zend_result accel_post_startup(void) { zend_function *func; @@ -3424,6 +3431,7 @@ static zend_result accel_post_startup(void) /* Override compiler */ accelerator_orig_compile_file = zend_compile_file; zend_compile_file = persistent_compile_file; + zend_store_deoptimized_op_array = store_deoptimized_op_array; /* Override stream opener function (to eliminate open() call caused by * include/require statements ) */ From 82ec0260a7db8b02c107777935b1ea4c3b6c2c0d Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 16:36:59 +0100 Subject: [PATCH 14/15] Use zend_compile_file() for deoptimization --- Zend/zend_compile.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 26ca574178cf..fc9bb64b7b79 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1359,6 +1359,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func) HashTable saved_functions; zend_hash_init(&saved_functions, 8, NULL, NULL, 0); + // FIXME: This is slow, better to just skip decl of functions in the deoptimization case. const zend_string *filename = func->op_array.filename; zend_string *key; zval *zv; @@ -1384,7 +1385,7 @@ ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func) zend_file_handle file_handle; zend_stream_init_filename_ex(&file_handle, func->op_array.filename); - zend_op_array *main_op_array = compile_file(&file_handle, ZEND_INCLUDE); + zend_op_array *main_op_array = zend_compile_file(&file_handle, ZEND_INCLUDE); zend_destroy_file_handle(&file_handle); CG(compiler_options) = orig_compiler_options; @@ -1411,6 +1412,10 @@ ZEND_API zend_function *zend_get_deoptimized_function(zend_function *func) } /* Cache the result on the original op_array. */ + // FIXME: We should either somehow only compile the relevant function, or at + // least link all deoptimized functions to their optimized counterparts. + // Otherwise we'll trigger a deoptimization for each function, while also + // persisting the entire script every time. if (deopt) { if (!zend_store_deoptimized_op_array) { func->op_array.deoptimized = &deopt->op_array; From 3dc07b277dd49b7cbaa4fac8281d9b6e99fc9e2d Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 5 Mar 2026 17:43:12 +0100 Subject: [PATCH 15/15] Fix persisting/unpersisting of unoptimized files --- ext/opcache/ZendAccelerator.c | 81 +++++++++++-------- .../tests/ns_global_func_assumption_001.phpt | 23 ++++++ 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index e82c8120298c..7f7ed124968b 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -1611,25 +1611,27 @@ static zend_persistent_script *cache_script_in_shared_memory(zend_persistent_scr /* Check if we still need to put the file into the cache (may be it was * already stored by another process. This final check is done under * exclusive lock) */ - bucket = zend_accel_hash_find_entry(&ZCSG(hash), new_persistent_script->script.filename); - if (bucket) { - zend_persistent_script *existing_persistent_script = (zend_persistent_script *)bucket->data; - - if (!existing_persistent_script->corrupted) { - if (key && - (!ZCG(accel_directives).validate_timestamps || - (new_persistent_script->timestamp == existing_persistent_script->timestamp))) { - zend_accel_add_key(key, bucket); - } - zend_shared_alloc_unlock(); + if (!(CG(compiler_options) & ZEND_COMPILE_DEOPTIMIZED)) { + bucket = zend_accel_hash_find_entry(&ZCSG(hash), new_persistent_script->script.filename); + if (bucket) { + zend_persistent_script *existing_persistent_script = (zend_persistent_script *)bucket->data; + + if (!existing_persistent_script->corrupted) { + if (key && + (!ZCG(accel_directives).validate_timestamps || + (new_persistent_script->timestamp == existing_persistent_script->timestamp))) { + zend_accel_add_key(key, bucket); + } + zend_shared_alloc_unlock(); #if 1 - /* prefer the script already stored in SHM */ - free_persistent_script(new_persistent_script, 1); - *from_shared_memory = true; - return existing_persistent_script; + /* prefer the script already stored in SHM */ + free_persistent_script(new_persistent_script, 1); + *from_shared_memory = true; + return existing_persistent_script; #else - return new_persistent_script; + return new_persistent_script; #endif + } } } @@ -1686,26 +1688,28 @@ static zend_persistent_script *cache_script_in_shared_memory(zend_persistent_scr } /* store script structure in the hash table */ - bucket = zend_accel_hash_update(&ZCSG(hash), new_persistent_script->script.filename, 0, new_persistent_script); - if (bucket) { - zend_accel_error(ACCEL_LOG_INFO, "Cached script '%s'", ZSTR_VAL(new_persistent_script->script.filename)); - if (key && - /* key may contain non-persistent PHAR aliases (see issues #115 and #149) */ - !zend_string_starts_with_literal(key, "phar://") && - !zend_string_equals(new_persistent_script->script.filename, key)) { - /* link key to the same persistent script in hash table */ - zend_string *new_key = accel_new_interned_key(key); - - if (new_key) { - if (zend_accel_hash_update(&ZCSG(hash), new_key, 1, bucket)) { - zend_accel_error(ACCEL_LOG_INFO, "Added key '%s'", ZSTR_VAL(key)); + if (!(CG(compiler_options) & ZEND_COMPILE_DEOPTIMIZED)) { + bucket = zend_accel_hash_update(&ZCSG(hash), new_persistent_script->script.filename, 0, new_persistent_script); + if (bucket) { + zend_accel_error(ACCEL_LOG_INFO, "Cached script '%s'", ZSTR_VAL(new_persistent_script->script.filename)); + if (key && + /* key may contain non-persistent PHAR aliases (see issues #115 and #149) */ + !zend_string_starts_with_literal(key, "phar://") && + !zend_string_equals(new_persistent_script->script.filename, key)) { + /* link key to the same persistent script in hash table */ + zend_string *new_key = accel_new_interned_key(key); + + if (new_key) { + if (zend_accel_hash_update(&ZCSG(hash), new_key, 1, bucket)) { + zend_accel_error(ACCEL_LOG_INFO, "Added key '%s'", ZSTR_VAL(key)); + } else { + zend_accel_error(ACCEL_LOG_DEBUG, "No more entries in hash table!"); + ZSMMG(memory_exhausted) = true; + zend_accel_schedule_restart_if_necessary(ACCEL_RESTART_HASH); + } } else { - zend_accel_error(ACCEL_LOG_DEBUG, "No more entries in hash table!"); - ZSMMG(memory_exhausted) = true; - zend_accel_schedule_restart_if_necessary(ACCEL_RESTART_HASH); + zend_accel_schedule_restart_if_necessary(ACCEL_RESTART_OOM); } - } else { - zend_accel_schedule_restart_if_necessary(ACCEL_RESTART_OOM); } } } @@ -1837,6 +1841,7 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl CG(compiler_options) |= ZEND_COMPILE_NO_CONSTANT_SUBSTITUTION; CG(compiler_options) |= ZEND_COMPILE_IGNORE_OTHER_FILES; CG(compiler_options) |= ZEND_COMPILE_IGNORE_OBSERVER; + CG(compiler_options) |= (orig_compiler_options & ZEND_COMPILE_DEOPTIMIZED); #ifdef ZEND_WIN32 /* On Windows, don't compile with internal classes. Shm may be attached from different * processes with internal classes living in different addresses. */ @@ -2021,6 +2026,12 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) zend_string *key = NULL; bool from_shared_memory; /* if the script we've got is stored in SHM */ + if (CG(compiler_options) & ZEND_COMPILE_DEOPTIMIZED) { + HANDLE_BLOCK_INTERRUPTIONS(); + SHM_UNPROTECT(); + goto skip_caching; + } + if (!file_handle->filename || !ZCG(accelerator_enabled)) { /* The Accelerator is disabled, act as if without the Accelerator */ ZCG(cache_opline) = NULL; @@ -2169,6 +2180,8 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) /* If script was not found or invalidated by validate_timestamps */ if (!persistent_script) { + // FIXME: Skip miss counter? +skip_caching:; uint32_t old_const_num = zend_hash_next_free_element(EG(zend_constants)); zend_op_array *op_array; diff --git a/ext/opcache/tests/ns_global_func_assumption_001.phpt b/ext/opcache/tests/ns_global_func_assumption_001.phpt index 0705ca4453f7..cc0b4490d3d9 100644 --- a/ext/opcache/tests/ns_global_func_assumption_001.phpt +++ b/ext/opcache/tests/ns_global_func_assumption_001.phpt @@ -56,4 +56,27 @@ Ns\strlen: 0001 T2 = STRLEN CV0($string) 0002 T1 = ADD T2 T2 0003 RETURN T1 + +$_main: + ; (lines=6, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.php:%s +0000 INIT_FCALL 0 %d string("ns\\test") +0001 DO_UCALL +0002 INCLUDE_OR_EVAL (require) string("%sns_global_func_assumption_001.inc") +0003 INIT_FCALL 0 %d string("ns\\test") +0004 DO_UCALL +0005 RETURN int(1) + +Ns\test: + ; (lines=7, args=0, vars=0, tmps=%d) + ; (after optimizer) + ; %sns_global_func_assumption_001.php:%s +0000 INIT_NS_FCALL_BY_NAME 1 string("Ns\\var_dump") +0001 INIT_NS_FCALL_BY_NAME 1 string("Ns\\strlen") +0002 SEND_VAL_EX string("Hello world") 1 +0003 V0 = DO_FCALL_BY_NAME +0004 SEND_VAR_NO_REF_EX V0 1 +0005 DO_FCALL_BY_NAME +0006 RETURN null int(22)