From 40ddf7990495b0878fb4ca47e45cc63f29590897 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 5 Oct 2024 22:55:09 +0200 Subject: [PATCH] Fixed GH-16233: Observer segfault when calling user function in internal function via trampoline In the test, I have an internal `__call` function for `_ZendTestMagicCallForward` that calls the global function with name `$name` via `call_user_function`. Note that observer writes the pointer to the previously observed frame in the last temporary of the new call frame (`*prev_observed_frame`). The following happens: First, we call `$test->callee`, this will be handled via a trampoline with T=2 for the two arguments. The call frame is allocated at this point. This call frame is not observed because it has `ZEND_ACC_CALL_VIA_TRAMPOLINE` set. Next we use `ZEND_CALL_TRAMPOLINE` to call the trampoline, this reuses the stack frame allocated earlier with T=2, but this time it is observed. The pointer to the previous frame is written outside of the call frame because `T` is too small (should be 3). We are now in the internal function `_ZendTestMagicCallForward::__call` where we call the global function `callee`. This will push a new call frame which will overlap `*prev_observed_frame`. This value gets overwritten by `zend_init_func_execute_data` when `EX(opline)` is set because `*prev_observed_frame` overlaps with `EX(opline)`. From now on, `*prev_observed_frame` is corrupted. When `zend_observer_fcall_end` is called this will result in reading wrong value `*prev_observed_frame` into `current_observed_frame`. This causes issues in `zend_observer_fcall_end_all` leading to the segfault we observe. Despite function with `ZEND_ACC_CALL_VIA_TRAMPOLINE` not being observed, the reuse of call frames makes problems when `T` is not large enough. To fix this, we make sure to add 1 to `T` if `ZEND_OBSERVER_ENABLED` is true. --- Zend/zend_object_handlers.c | 4 +++- ext/zend_test/test.c | 19 +++++++++++++++++++ ext/zend_test/test.stub.php | 5 +++++ ext/zend_test/test_arginfo.h | 21 ++++++++++++++++++++- ext/zend_test/tests/gh16233.phpt | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 ext/zend_test/tests/gh16233.phpt diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index d4586e53e4b4..def9695ed98b 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -30,6 +30,7 @@ #include "zend_closures.h" #include "zend_compile.h" #include "zend_hash.h" +#include "zend_observer.h" #define DEBUG_OBJECT_HANDLERS 0 @@ -1294,7 +1295,8 @@ ZEND_API zend_function *zend_get_call_trampoline_func(zend_class_entry *ce, zend * value so that it doesn't contain garbage when the engine allocates space for the next stack * frame. This didn't cause any issues until now due to "lucky" structure layout. */ func->last_var = 0; - func->T = (fbc->type == ZEND_USER_FUNCTION)? MAX(fbc->op_array.last_var + fbc->op_array.T, 2) : 2; + uint32_t min_T = 2 + ZEND_OBSERVER_ENABLED; + func->T = (fbc->type == ZEND_USER_FUNCTION)? MAX(fbc->op_array.last_var + fbc->op_array.T, min_T) : min_T; func->filename = (fbc->type == ZEND_USER_FUNCTION)? fbc->op_array.filename : ZSTR_EMPTY_ALLOC(); func->line_start = (fbc->type == ZEND_USER_FUNCTION)? fbc->op_array.line_start : 0; func->line_end = (fbc->type == ZEND_USER_FUNCTION)? fbc->op_array.line_end : 0; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 2df6c2027498..8989413317d7 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -879,6 +879,23 @@ static ZEND_METHOD(_ZendTestMagicCall, __call) RETURN_ARR(zend_new_pair(&name_zv, arguments)); } +static ZEND_METHOD(_ZendTestMagicCallForward, __call) +{ + zend_string *name; + zval *arguments; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(name) + Z_PARAM_ARRAY(arguments) + ZEND_PARSE_PARAMETERS_END(); + + ZEND_IGNORE_VALUE(arguments); + + zval func; + ZVAL_STR(&func, name); + call_user_function(NULL, NULL, &func, return_value, 0, NULL); +} + PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("zend_test.replace_zend_execute_ex", "0", PHP_INI_SYSTEM, OnUpdateBool, replace_zend_execute_ex, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.register_passes", "0", PHP_INI_SYSTEM, OnUpdateBool, register_passes, zend_zend_test_globals, zend_test_globals) @@ -993,6 +1010,8 @@ PHP_MINIT_FUNCTION(zend_test) zend_test_magic_call = register_class__ZendTestMagicCall(); + register_class__ZendTestMagicCallForward(); + zend_register_functions(NULL, ext_function_legacy, NULL, EG(current_module)->type); // Loading via dl() not supported with the observer API diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 7dc348934400..8a605bdcd328 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -52,6 +52,11 @@ class _ZendTestMagicCall public function __call(string $name, array $args): mixed {} } + class _ZendTestMagicCallForward + { + public function __call(string $name, array $args): mixed {} + } + class _ZendTestChildClass extends _ZendTestClass { public function returnsThrowable(): Exception {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 19ea9e7a2adf..9c0735a1d6f8 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 07ce28cd75080118509ac0d30d8ce5ef54110747 */ + * Stub hash: 5d861e05edfd57c385167b11b8b1ea977ed130a2 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -161,6 +161,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestMagicCall___call, ZEND_ARG_TYPE_INFO(0, args, IS_ARRAY, 0) ZEND_END_ARG_INFO() +#define arginfo_class__ZendTestMagicCallForward___call arginfo_class__ZendTestMagicCall___call + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class__ZendTestChildClass_returnsThrowable, 0, 0, Exception, 0) ZEND_END_ARG_INFO() @@ -245,6 +247,7 @@ static ZEND_METHOD(_ZendTestClass, returnsStatic); static ZEND_METHOD(_ZendTestClass, returnsThrowable); static ZEND_METHOD(_ZendTestClass, variadicTest); static ZEND_METHOD(_ZendTestMagicCall, __call); +static ZEND_METHOD(_ZendTestMagicCallForward, __call); static ZEND_METHOD(_ZendTestChildClass, returnsThrowable); static ZEND_METHOD(_ZendTestTrait, testMethod); static ZEND_METHOD(ZendTestParameterAttribute, __construct); @@ -332,6 +335,12 @@ static const zend_function_entry class__ZendTestMagicCall_methods[] = { }; +static const zend_function_entry class__ZendTestMagicCallForward_methods[] = { + ZEND_ME(_ZendTestMagicCallForward, __call, arginfo_class__ZendTestMagicCallForward___call, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + + static const zend_function_entry class__ZendTestChildClass_methods[] = { ZEND_ME(_ZendTestChildClass, returnsThrowable, arginfo_class__ZendTestChildClass_returnsThrowable, ZEND_ACC_PUBLIC) ZEND_FE_END @@ -532,6 +541,16 @@ static zend_class_entry *register_class__ZendTestMagicCall(void) return class_entry; } +static zend_class_entry *register_class__ZendTestMagicCallForward(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestMagicCallForward", class__ZendTestMagicCallForward_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + + return class_entry; +} + static zend_class_entry *register_class__ZendTestChildClass(zend_class_entry *class_entry__ZendTestClass) { zend_class_entry ce, *class_entry; diff --git a/ext/zend_test/tests/gh16233.phpt b/ext/zend_test/tests/gh16233.phpt new file mode 100644 index 000000000000..e3143b6c7ce1 --- /dev/null +++ b/ext/zend_test/tests/gh16233.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-16233 (Observer segfault when calling user function in internal function via trampoline) +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.enabled=1 +zend_test.observer.show_output=1 +zend_test.observer.observe_all=1 +--FILE-- +callee(); +echo "done\n"; + +?> +--EXPECTF-- + + + + <_ZendTestMagicCallForward::__call> + + +in callee + + +done +