diff --git a/NEWS b/NEWS index 12d15e699f1f..c0dfd3c00838 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,9 @@ PHP NEWS . Fixed bug GH-24321 (ZTS: global_map_ptr_last survives zend_shutdown(), so an in-process engine restart keeps growing the map_ptr table). (David Carlier) + . Fixed temporaries leaking when a VM interrupt throws at the op that frees + them, e.g. as a foreach loop over a generator exits. + (C. Scott Ananian) - Date: . Fixed bug GH-20503 (Assertion failure with ext/date DateInterval property diff --git a/Zend/tests/generators/foreach_vm_interrupt_on_exit.phpt b/Zend/tests/generators/foreach_vm_interrupt_on_exit.phpt new file mode 100644 index 000000000000..acf9e35e0c6d --- /dev/null +++ b/Zend/tests/generators/foreach_vm_interrupt_on_exit.phpt @@ -0,0 +1,82 @@ +--TEST-- +VM interrupt throwing as foreach over a generator exits the loop +--DESCRIPTION-- +When the generator finishes, FE_RESET and FE_FETCH jump forward to the loop's +FE_FREE. An interrupt that throws there must not leak the iterator. +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.enabled=1 +zend_test.observer.show_output=0 +zend_test.observer.observe_function_names=release +zend_test.observer.set_vm_interrupt_on_end=1 +--FILE-- +getMessage(), "\n"; + } +} + +test('foreach', function () { + foreach (gen() as $v) { + echo "value: $v\n"; + } +}); + +test('foreach, empty', function () { + foreach (gen_empty() as $v) { + echo "value: $v\n"; + } +}); + +test('foreach by reference', function () { + foreach (gen_ref() as &$v) { + echo "value: $v\n"; + } +}); +?> +--EXPECT-- +foreach: +value: 1 +release() +Exception: VM interrupt +foreach, empty: +release() +Exception: VM interrupt +foreach by reference: +value: 1 +release() +Exception: VM interrupt diff --git a/Zend/tests/vm_interrupt_forward_jump.phpt b/Zend/tests/vm_interrupt_forward_jump.phpt new file mode 100644 index 000000000000..dad2fef0d24b --- /dev/null +++ b/Zend/tests/vm_interrupt_forward_jump.phpt @@ -0,0 +1,93 @@ +--TEST-- +VM interrupt pending at a forward jump onto the op that frees a temporary +--DESCRIPTION-- +A jump whose target frees or consumes a temporary that is live across the +jump: the target is the end of that temporary's live range, so an exception +thrown there by an interrupt must not leak the temporary. +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.enabled=1 +zend_test.observer.show_output=0 +zend_test.observer.observe_function_names=release +zend_test.observer.set_vm_interrupt_on_end=1 +--FILE-- +getMessage(), "\n"; + } +} + +test('ternary', function () { + $c = value(); + $x = $c ? $c . release() : 'c'; +}); + +test('ternary, array element', function () { + $c = value(); + $a = []; + $a['k'] = $c ? $c . release() : 'c'; +}); + +// Interrupts are checked on return from a function in the CALL VM +test('interpolated last', function () { + $f = 'release'; + $x = "<{$f()}"; +}); + +test('match', function () { + $c = value(); + $x = match ($c) { + 'aaa' => $c . release(), + default => 'c', + }; +}); + +test('switch', function () { + switch (value()) { + case 'aaa': + release(); + break; + default: + echo "default\n"; + } +}); +?> +--EXPECT-- +ternary: +release() +Exception: VM interrupt +ternary, array element: +release() +Exception: VM interrupt +interpolated last: +release() +Exception: VM interrupt +match: +release() +Exception: VM interrupt +switch: +release() +Exception: VM interrupt diff --git a/Zend/tests/vm_interrupt_generator_throw.phpt b/Zend/tests/vm_interrupt_generator_throw.phpt new file mode 100644 index 000000000000..71c820280da2 --- /dev/null +++ b/Zend/tests/vm_interrupt_generator_throw.phpt @@ -0,0 +1,40 @@ +--TEST-- +VM interrupt handled while Generator::throw() resumes the generator +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.enabled=1 +zend_test.observer.show_output=0 +zend_test.observer.observe_function_names=gen +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- +getMessage(), PHP_EOL; + } + echo "resumed\n"; + yield 2; +} + +$g = gen(); +var_dump($g->current() instanceof Dtor); +var_dump($g->throw(new Exception('thrown'))); +unset($g); +echo "done\n"; +?> +--EXPECT-- +bool(true) +Exception: thrown +resumed +destroyed +int(2) +done diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index ae0b337ed2a6..a9f2490c55ff 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4687,6 +4687,51 @@ static void free_unfinished_calls(zend_execute_data *execute_data) } } +static void cleanup_live_var(zend_execute_data *execute_data, const zend_live_range *range, uint32_t op_num) /* {{{ */ +{ + uint32_t kind = range->var & ZEND_LIVE_MASK; + uint32_t var_num = range->var & ~ZEND_LIVE_MASK; + zval *var = EX_VAR(var_num); + + if (kind == ZEND_LIVE_TMPVAR) { + zval_ptr_dtor_nogc(var); + } else if (kind == ZEND_LIVE_NEW) { + zend_object *obj; + ZEND_ASSERT(Z_TYPE_P(var) == IS_OBJECT); + obj = Z_OBJ_P(var); + zend_object_store_ctor_failed(obj); + OBJ_RELEASE(obj); + } else if (kind == ZEND_LIVE_LOOP) { + if (Z_TYPE_P(var) != IS_ARRAY && Z_FE_ITER_P(var) != (uint32_t)-1) { + zend_hash_iterator_del(Z_FE_ITER_P(var)); + } + zval_ptr_dtor_nogc(var); + } else if (kind == ZEND_LIVE_ROPE) { + zend_string **rope = (zend_string **)var; + zend_op *last = EX(func)->op_array.opcodes + op_num; + while ((last->opcode != ZEND_ROPE_ADD && last->opcode != ZEND_ROPE_INIT) + || last->result.var != var_num) { + ZEND_ASSERT(last >= EX(func)->op_array.opcodes); + last--; + } + if (last->opcode == ZEND_ROPE_INIT) { + zend_string_release_ex(*rope, 0); + } else { + int j = last->extended_value; + do { + zend_string_release_ex(rope[j], 0); + } while (j--); + } + } else if (kind == ZEND_LIVE_SILENCE) { + /* restore previous error_reporting value */ + if (E_HAS_ONLY_FATAL_ERRORS(EG(error_reporting)) + && !E_HAS_ONLY_FATAL_ERRORS(Z_LVAL_P(var))) { + EG(error_reporting) = Z_LVAL_P(var); + } + } +} +/* }}} */ + static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, uint32_t catch_op_num) /* {{{ */ { int i; @@ -4698,10 +4743,6 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, break; } else if (op_num < range->end) { if (!catch_op_num || catch_op_num >= range->end) { - uint32_t kind = range->var & ZEND_LIVE_MASK; - uint32_t var_num = range->var & ~ZEND_LIVE_MASK; - zval *var = EX_VAR(var_num); - /* Handle the split range for loop vars */ if (catch_op_num) { zend_op *final_op = EX(func)->op_array.opcodes + range->end; @@ -4712,45 +4753,116 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, } } - if (kind == ZEND_LIVE_TMPVAR) { - zval_ptr_dtor_nogc(var); - } else if (kind == ZEND_LIVE_NEW) { - zend_object *obj; - ZEND_ASSERT(Z_TYPE_P(var) == IS_OBJECT); - obj = Z_OBJ_P(var); - zend_object_store_ctor_failed(obj); - OBJ_RELEASE(obj); - } else if (kind == ZEND_LIVE_LOOP) { - if (Z_TYPE_P(var) != IS_ARRAY && Z_FE_ITER_P(var) != (uint32_t)-1) { - zend_hash_iterator_del(Z_FE_ITER_P(var)); - } - zval_ptr_dtor_nogc(var); - } else if (kind == ZEND_LIVE_ROPE) { - zend_string **rope = (zend_string **)var; - zend_op *last = EX(func)->op_array.opcodes + op_num; - while ((last->opcode != ZEND_ROPE_ADD && last->opcode != ZEND_ROPE_INIT) - || last->result.var != var_num) { - ZEND_ASSERT(last >= EX(func)->op_array.opcodes); - last--; - } - if (last->opcode == ZEND_ROPE_INIT) { - zend_string_release_ex(*rope, 0); - } else { - int j = last->extended_value; - do { - zend_string_release_ex(rope[j], 0); - } while (j--); - } - } else if (kind == ZEND_LIVE_SILENCE) { - /* restore previous error_reporting value */ - if (E_HAS_ONLY_FATAL_ERRORS(EG(error_reporting)) - && !E_HAS_ONLY_FATAL_ERRORS(Z_LVAL_P(var))) { - EG(error_reporting) = Z_LVAL_P(var); - } - } + cleanup_live_var(execute_data, range, op_num); + } + } + } +} +/* }}} */ + +static void zend_cleanup_interrupted_operand(zend_execute_data *execute_data, const zend_op *def, const zend_op *throw_op) /* {{{ */ +{ + uint32_t op_num = throw_op - EX(func)->op_array.opcodes; + uint32_t var_num = def->result.var; + int i; + + if (!(def->result_type & (IS_TMP_VAR|IS_VAR))) { + return; + } + switch (def->opcode) { + /* These don't define a value that has to be destroyed, + * or have their own kind of live range (see emit_live_range()). */ + case ZEND_ADD_ARRAY_ELEMENT: + case ZEND_ADD_ARRAY_UNPACK: + case ZEND_ROPE_INIT: + case ZEND_ROPE_ADD: + case ZEND_JMPZ_EX: + case ZEND_JMPNZ_EX: + case ZEND_BOOL: + case ZEND_BOOL_NOT: + case ZEND_FETCH_CLASS: + case ZEND_DECLARE_ANON_CLASS: + case ZEND_FAST_CALL: + case ZEND_BEGIN_SILENCE: + case ZEND_FE_RESET_R: + case ZEND_FE_RESET_RW: + case ZEND_NEW: + case ZEND_SEPARATE: + return; + } + /* A live range that ends at throw_op was cleaned up already; one that + * continues past it (throw_op does not consume the var) is cleaned up by + * ZEND_HANDLE_EXCEPTION. */ + for (i = 0; i < EX(func)->op_array.last_live_range; i++) { + const zend_live_range *range = &EX(func)->op_array.live_range[i]; + if (range->start > op_num) { + break; + } else if (op_num <= range->end && (range->var & ~ZEND_LIVE_MASK) == var_num) { + return; + } + } + zval_ptr_dtor_nogc(EX_VAR(var_num)); +} +/* }}} */ + +/* Called when EG(exception) is set after the interrupt function was called. + * The VM checks for interrupts after setting opline to the next op (e.g. the + * target of a jump), so an exception that the interrupt function throws is + * attributed to an op that did not run. */ +ZEND_API void zend_cleanup_interrupted_op(bool exception_was_pending) /* {{{ */ +{ + zend_execute_data *execute_data = EG(current_execute_data); + const zend_op *throw_op = EG(opline_before_exception); + uint32_t op_num; + int i; + + if (!throw_op) { + return; + } + + /* If the exception was already pending when the interrupt was handled + * (e.g. when Generator::throw() resumes a generator), throw_op did run. */ + if (!exception_was_pending && EX(opline)->opcode == ZEND_HANDLE_EXCEPTION) { + /* Live ranges end at the op that frees the var (e.g. the FE_FREE of a + * foreach loop), so ZEND_HANDLE_EXCEPTION does not free vars whose live + * range ends at throw_op. throw_op did not free them, so do it here. */ + op_num = throw_op - EX(func)->op_array.opcodes; + for (i = 0; i < EX(func)->op_array.last_live_range; i++) { + const zend_live_range *range = &EX(func)->op_array.live_range[i]; + if (range->start > op_num) { + break; + } else if (range->end == op_num) { + cleanup_live_var(execute_data, range, op_num); + } + } + + /* A var defined by the op just before throw_op has no live range (it would + * be trivial), e.g. the result of the last branch of a ternary. */ + if (op_num > 0) { + const zend_op *def = throw_op - 1; + + if ((throw_op->op1_type & (IS_TMP_VAR|IS_VAR)) && throw_op->op1.var == def->result.var) { + zend_cleanup_interrupted_operand(execute_data, def, throw_op); + } + if ((throw_op->op2_type & (IS_TMP_VAR|IS_VAR)) && throw_op->op2.var == def->result.var) { + zend_cleanup_interrupted_operand(execute_data, def, throw_op); + } + if (op_num + 1 < EX(func)->op_array.last + && (throw_op+1)->opcode == ZEND_OP_DATA + && ((throw_op+1)->op1_type & (IS_TMP_VAR|IS_VAR)) && (throw_op+1)->op1.var == def->result.var) { + zend_cleanup_interrupted_operand(execute_data, def, throw_op); } } } + + /* We have to UNDEF result, because ZEND_HANDLE_EXCEPTION is going to free it */ + if (throw_op->result_type & (IS_TMP_VAR|IS_VAR) + && throw_op->opcode != ZEND_ADD_ARRAY_ELEMENT + && throw_op->opcode != ZEND_ADD_ARRAY_UNPACK + && throw_op->opcode != ZEND_ROPE_INIT + && throw_op->opcode != ZEND_ROPE_ADD) { + ZVAL_UNDEF(EX_VAR(throw_op->result.var)); + } } /* }}} */ diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 1dd50ab8a69e..55e22ba88851 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -433,6 +433,7 @@ ZEND_API void zend_clean_and_cache_symbol_table(zend_array *symbol_table); ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *execute_data); ZEND_API void zend_unfinished_calls_gc(zend_execute_data *execute_data, zend_execute_data *call, uint32_t op_num, zend_get_gc_buffer *buf); ZEND_API void zend_cleanup_unfinished_execution(zend_execute_data *execute_data, uint32_t op_num, uint32_t catch_op_num); +ZEND_API void zend_cleanup_interrupted_op(bool exception_was_pending); ZEND_API ZEND_ATTRIBUTE_DEPRECATED HashTable *zend_unfinished_execution_gc(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer); ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer, bool suspended_by_yield); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index df255c5a868f..572702e20def 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10431,20 +10431,11 @@ ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) zend_handle_delayed_oom(); HANDLE_EXCEPTION(); } else if (zend_interrupt_function) { + bool exception_was_pending = EG(exception) != NULL; + zend_interrupt_function(execute_data); if (EG(exception)) { - /* We have to UNDEF result, because ZEND_HANDLE_EXCEPTION is going to free it */ - const zend_op *throw_op = EG(opline_before_exception); - - if (throw_op - && throw_op->result_type & (IS_TMP_VAR|IS_VAR) - && throw_op->opcode != ZEND_ADD_ARRAY_ELEMENT - && throw_op->opcode != ZEND_ADD_ARRAY_UNPACK - && throw_op->opcode != ZEND_ROPE_INIT - && throw_op->opcode != ZEND_ROPE_ADD) { - ZVAL_UNDEF(ZEND_CALL_VAR(EG(current_execute_data), throw_op->result.var)); - - } + zend_cleanup_interrupted_op(exception_was_pending); } ZEND_VM_ENTER(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 4b313f20cd39..cd6ac5602afd 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -3886,20 +3886,11 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_interrupt_he zend_handle_delayed_oom(); HANDLE_EXCEPTION(); } else if (zend_interrupt_function) { + bool exception_was_pending = EG(exception) != NULL; + zend_interrupt_function(execute_data); if (EG(exception)) { - /* We have to UNDEF result, because ZEND_HANDLE_EXCEPTION is going to free it */ - const zend_op *throw_op = EG(opline_before_exception); - - if (throw_op - && throw_op->result_type & (IS_TMP_VAR|IS_VAR) - && throw_op->opcode != ZEND_ADD_ARRAY_ELEMENT - && throw_op->opcode != ZEND_ADD_ARRAY_UNPACK - && throw_op->opcode != ZEND_ROPE_INIT - && throw_op->opcode != ZEND_ROPE_ADD) { - ZVAL_UNDEF(ZEND_CALL_VAR(EG(current_execute_data), throw_op->result.var)); - - } + zend_cleanup_interrupted_op(exception_was_pending); } ZEND_VM_ENTER(); } diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 9504b5bf79ec..dea11f42d2ee 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -3242,20 +3242,10 @@ static void ZEND_FASTCALL zend_jit_free_trampoline_helper(zend_function *func) zend_free_trampoline(func); } -static void ZEND_FASTCALL zend_jit_exception_in_interrupt_handler_helper(void) +static void ZEND_FASTCALL zend_jit_exception_in_interrupt_handler_helper(const zend_object *prev_exception) { if (EG(exception)) { - /* We have to UNDEF result, because ZEND_HANDLE_EXCEPTION is going to free it */ - const zend_op *throw_op = EG(opline_before_exception); - - if (throw_op - && throw_op->result_type & (IS_TMP_VAR|IS_VAR) - && throw_op->opcode != ZEND_ADD_ARRAY_ELEMENT - && throw_op->opcode != ZEND_ADD_ARRAY_UNPACK - && throw_op->opcode != ZEND_ROPE_INIT - && throw_op->opcode != ZEND_ROPE_ADD) { - ZVAL_UNDEF(ZEND_CALL_VAR(EG(current_execute_data), throw_op->result.var)); - } + zend_cleanup_interrupted_op(prev_exception != NULL); } } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 830dbdf32721..f52fbc0f3178 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2073,10 +2073,12 @@ static int zend_jit_interrupt_handler_stub(zend_jit_ctx *jit) ir_IF_FALSE(if_delayed_oom); if (zend_interrupt_function) { + ir_ref prev_exception = ir_LOAD_A(jit_EG(exception)); + ir_CALL_1(IR_VOID, ir_CONST_FUNC(zend_interrupt_function), jit_FP(jit)); if_exception = ir_IF(ir_LOAD_A(jit_EG(exception))); ir_IF_TRUE(if_exception); - ir_CALL(IR_VOID, ir_CONST_FUNC(zend_jit_exception_in_interrupt_handler_helper)); + ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_jit_exception_in_interrupt_handler_helper), prev_exception); ir_MERGE_WITH_EMPTY_FALSE(if_exception); jit_STORE_FP(jit, ir_LOAD_A(jit_EG(current_execute_data))); diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index 6f3950e87fa6..98bbce214eea 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -22,6 +22,7 @@ #include "zend_smart_str.h" #include "ext/standard/php_var.h" #include "zend_generators.h" +#include "zend_exceptions.h" static zend_observer_fcall_handlers observer_fcall_init(zend_execute_data *execute_data); @@ -129,6 +130,11 @@ static void observer_end(zend_execute_data *execute_data, zval *retval) { assert_observer_opline(execute_data); + if (ZT_G(observer_set_vm_interrupt_on_end) && !EG(exception)) { + ZT_G(observer_throw_on_vm_interrupt) = true; + zend_atomic_bool_store_ex(&EG(vm_interrupt), true); + } + if (!ZT_G(observer_show_output)) { return; } @@ -158,6 +164,11 @@ static void zend_test_interrupt_function(zend_execute_data *execute_data) if (zend_test_prev_interrupt_function) { zend_test_prev_interrupt_function(execute_data); } + + if (ZT_G(observer_throw_on_vm_interrupt)) { + ZT_G(observer_throw_on_vm_interrupt) = false; + zend_throw_exception(NULL, "VM interrupt", 0); + } } static void observer_show_init(zend_function *fbc) @@ -377,6 +388,7 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("zend_test.observer.show_opcode_in_user_handler", "", PHP_INI_SYSTEM, OnUpdateString, observer_show_opcode_in_user_handler, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.set_vm_interrupt_on_begin", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_set_vm_interrupt_on_begin, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.set_vm_interrupt_on_begin_without_callback", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_set_vm_interrupt_on_begin_without_callback, zend_zend_test_globals, zend_test_globals) + STD_PHP_INI_BOOLEAN("zend_test.observer.set_vm_interrupt_on_end", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_set_vm_interrupt_on_end, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_init", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_init, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_switch", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_switch, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_destroy", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_destroy, zend_zend_test_globals, zend_test_globals) @@ -419,7 +431,7 @@ void zend_test_observer_init(INIT_FUNC_ARGS) zend_execute_internal = zend_test_execute_internal; } - if (ZT_G(observer_set_vm_interrupt_on_begin)) { + if (ZT_G(observer_set_vm_interrupt_on_begin) || ZT_G(observer_set_vm_interrupt_on_end)) { zend_test_prev_interrupt_function = zend_interrupt_function; zend_interrupt_function = zend_test_interrupt_function; } diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index 9ab27f5a54f6..738dcb27641f 100644 --- a/ext/zend_test/php_test.h +++ b/ext/zend_test/php_test.h @@ -47,6 +47,8 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test) char *observer_show_opcode_in_user_handler; int observer_set_vm_interrupt_on_begin; int observer_set_vm_interrupt_on_begin_without_callback; + int observer_set_vm_interrupt_on_end; + bool observer_throw_on_vm_interrupt; int observer_nesting_depth; int observer_fiber_init; int observer_fiber_switch; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 42619c626027..52edbed9ae66 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1586,6 +1586,7 @@ PHP_RINIT_FUNCTION(zend_test) ALLOC_HASHTABLE(ZT_G(global_weakmap)); zend_hash_init(ZT_G(global_weakmap), 8, NULL, ZVAL_PTR_DTOR, 0); ZT_G(observer_nesting_depth) = 0; + ZT_G(observer_throw_on_vm_interrupt) = false; zend_test_mm_custom_handlers_rinit(); return SUCCESS; }