From 5a8e1b5a6f8f94c54e5557861bfa6fd4617eb224 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 24 Jul 2026 13:53:48 -0400 Subject: [PATCH] Fix GH-22878: Use-after-free of callable via autoloader Validating an array or string callable runs user code before its borrowed method name and object are used: a string class name can trigger an autoloader, and a compound "Class::method" name emits an E_DEPRECATED that reaches a user error handler. Either can free or mutate the callable, leaving the method string and $this dangling. Copy the method string before the reentrant lookup, and hold the callable array across INIT_USER_CALL's validation and frame build so the object survives to the call. This also covers call_user_func_array(), $cb(), and referenced or reference-wrapped array members. Fixes GH-22878 --- NEWS | 1 + Zend/tests/gh22878.phpt | 41 ++++++++++++++++ Zend/zend_API.c | 17 ++++++- Zend/zend_execute.c | 10 ++-- Zend/zend_vm_def.h | 16 +++++++ Zend/zend_vm_execute.h | 100 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/gh22878.phpt diff --git a/NEWS b/NEWS index 48864929c225..7d630d137536 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,7 @@ PHP NEWS . Fixed bug GH-18847 (SEGV in zend_fetch_debug_backtrace() when the memory limit is reached while the tracing JIT enters a call frame). (Arnaud, iliaal) + . Fixed bug GH-22878 (Use-after-free of callable via autoloader). (iliaal) - DOM: . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD diff --git a/Zend/tests/gh22878.phpt b/Zend/tests/gh22878.phpt new file mode 100644 index 000000000000..c69127f392ae --- /dev/null +++ b/Zend/tests/gh22878.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-22878 (Use-after-free of callable via autoloader) +--FILE-- + +--EXPECT-- +256 +256 +256 +256 +13 +done diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 72f4727eeddf..1230017e6c5b 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4194,6 +4194,9 @@ ZEND_API bool zend_is_callable_at_frame( bool ret; zend_fcall_info_cache fcc_local; bool strict_class = false; + zval callable_copy; + + ZVAL_UNDEF(&callable_copy); if (fcc == NULL) { fcc = &fcc_local; @@ -4221,11 +4224,17 @@ ZEND_API bool zend_is_callable_at_frame( return 1; } + ZVAL_COPY(&callable_copy, callable); + callable = &callable_copy; + check_func: ret = zend_is_callable_check_func(callable, frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); if (fcc == &fcc_local) { zend_release_fcall_info_cache(fcc); } + if (!Z_ISUNDEF(callable_copy)) { + zval_ptr_dtor(&callable_copy); + } return ret; case IS_ARRAY: @@ -4259,7 +4268,11 @@ ZEND_API bool zend_is_callable_at_frame( return 1; } + ZVAL_COPY(&callable_copy, method); + callable = &callable_copy; + if (!zend_is_callable_check_class(Z_STR_P(obj), get_scope(frame), frame, fcc, &strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS)) { + zval_ptr_dtor(&callable_copy); return 0; } } else { @@ -4271,9 +4284,11 @@ ZEND_API bool zend_is_callable_at_frame( fcc->called_scope = fcc->calling_scope; return 1; } + + ZVAL_COPY(&callable_copy, method); + callable = &callable_copy; } - callable = method; goto check_func; } return 0; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 91cab1b63d2a..1aaa10cd2f14 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5257,23 +5257,27 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_array(const z } if (Z_TYPE_P(obj) == IS_STRING) { + zend_string *method_name = zend_string_copy(Z_STR_P(method)); zend_class_entry *called_scope = zend_fetch_class_by_name(Z_STR_P(obj), NULL, ZEND_FETCH_CLASS_DEFAULT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(called_scope == NULL)) { + zend_string_release(method_name); return NULL; } if (called_scope->get_static_method) { - fbc = called_scope->get_static_method(called_scope, Z_STR_P(method)); + fbc = called_scope->get_static_method(called_scope, method_name); } else { - fbc = zend_std_get_static_method(called_scope, Z_STR_P(method), NULL); + fbc = zend_std_get_static_method(called_scope, method_name, NULL); } if (UNEXPECTED(fbc == NULL)) { if (EXPECTED(!EG(exception))) { - zend_undefined_method(called_scope, Z_STR_P(method)); + zend_undefined_method(called_scope, method_name); } + zend_string_release(method_name); return NULL; } + zend_string_release(method_name); if (!(fbc->common.fn_flags & ZEND_ACC_STATIC)) { zend_non_static_method_call(fbc); if (fbc->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index d14230514b34..cf8de64380a5 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3982,9 +3982,16 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM) void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = GET_OP2_ZVAL_PTR(BP_VAR_R); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -3993,6 +4000,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM) * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } FREE_OP2(); HANDLE_EXCEPTION(); } @@ -4016,6 +4026,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM) call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } FREE_OP2(); if ((OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -4030,6 +4043,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM) init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); FREE_OP2(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 53bcdccd9719..bff363dab34d 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -7684,9 +7684,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -7695,6 +7702,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_CONST & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } HANDLE_EXCEPTION(); @@ -7719,6 +7729,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } + if ((IS_CONST & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -7733,6 +7747,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); @@ -10460,9 +10477,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -10471,6 +10495,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_TMP_VAR & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); HANDLE_EXCEPTION(); } @@ -10494,6 +10521,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); if ((IS_TMP_VAR & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -10508,6 +10538,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -13124,9 +13157,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -13135,6 +13175,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_CV & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } HANDLE_EXCEPTION(); @@ -13159,6 +13202,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } + if ((IS_CV & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -13173,6 +13220,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); @@ -60516,9 +60566,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -60527,6 +60584,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_CONST & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } HANDLE_EXCEPTION(); @@ -60551,6 +60611,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } + if ((IS_CONST & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -60565,6 +60629,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); @@ -63292,9 +63359,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -63303,6 +63377,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_TMP_VAR & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); HANDLE_EXCEPTION(); } @@ -63326,6 +63403,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); if ((IS_TMP_VAR & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -63340,6 +63420,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -65854,9 +65937,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE void *object_or_called_scope; zend_execute_data *call; uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC; + zval held_callable; SAVE_OPLINE(); function_name = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); + ZVAL_UNDEF(&held_callable); + ZVAL_DEREF(function_name); + if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) { + ZVAL_COPY(&held_callable, function_name); + function_name = &held_callable; + } if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { ZEND_ASSERT(!error); @@ -65865,6 +65955,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ if (!(IS_CV & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } HANDLE_EXCEPTION(); @@ -65889,6 +65982,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS; } + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } + if ((IS_CV & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) { if (call_info & ZEND_CALL_CLOSURE) { @@ -65903,6 +66000,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE init_func_run_time_cache(&func->op_array); } } else { + if (UNEXPECTED(!Z_ISUNDEF(held_callable))) { + zval_ptr_dtor(&held_callable); + } zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error); efree(error);