diff --git a/Zend/tests/generators/gh15851.phpt b/Zend/tests/generators/gh15851.phpt new file mode 100644 index 00000000000..8a7fa6294e2 --- /dev/null +++ b/Zend/tests/generators/gh15851.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-15851: Access on NULL when printing backtrace with freed generator +--FILE-- + +--EXPECTF-- +#0 %s(%d): Foo->__destruct() +#1 %s(%d): bar() diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index e2febd78ec7..bb8bb28bf6e 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -1883,6 +1883,16 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int } while (call && (limit == 0 || frameno < limit)) { + if (UNEXPECTED(!call->func)) { + /* This is the fake frame inserted for nested generators. Normally, + * this frame is preceded by the actual generator frame and then + * replaced by zend_generator_check_placeholder_frame() below. + * However, the frame is popped before cleaning the stack frame, + * which is observable by destructors. */ + call = zend_generator_check_placeholder_frame(call); + ZEND_ASSERT(call->func); + } + zend_execute_data *prev = call->prev_execute_data; if (!prev) { diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 11214465d06..9444f3d6253 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -1045,6 +1045,7 @@ do_repeat: object_init_ex(&ref, pce); memset(&execute_data, 0, sizeof(zend_execute_data)); + execute_data.func = (zend_function *) &zend_pass_function; EG(current_execute_data) = &execute_data; zend_call_known_instance_method_with_1_params( pce->constructor, Z_OBJ(ref), NULL, &arg);