Interrupt while internal frame is on the stack (#14627)

* Check VM interrupt while internal frame is on top

* Use tab instead of spaces

* fix frame used in interrupt and refactor

* remove unused failures for zend_jit_check_timeout

* Fix JIT support

Co-authored-by: Bob Weinand <bobwei9@hotmail.com>

* Fix the missing store to vm_interrupt

* Rename new functions

* Special case zend_interrupt_function in JIT code

* refactor to use ZEND_VM_SET_OPCODE_NO_INTERRUPT

* Split atomic exchange into load + store

It is difficult to determine performance of atomics sometimes. In this
case, the separate load+store is still correct, and a load does not
cause a modification, and might be faster for some platforms than an
exchange. A load+store is slower than an exchange, but we're fine
trading the penalty to the slow path and keeping the happy path faster.

---------

Co-authored-by: Bob Weinand <bobwei9@hotmail.com>
This commit is contained in:
Levi Morrison 2024-09-04 16:35:55 -06:00 committed by GitHub
parent 816aea797b
commit 6435bb5ae1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 122 additions and 47 deletions

View File

@ -377,6 +377,11 @@ PHP 8.4 INTERNALS UPGRADE NOTES
4. OpCode changes
========================
* DO_ICALL, DO_FCALL, and DO_FCALL_BY_NAME now call zend_interrupt_function
while the internal frame is still on the stack. This means interrupt handlers
will now see the internal call. If your interrupt handler does something like
switching EG(current_execute_data), it should not do so if an internal func
is on top.
* New FRAMELESS_ICALL_[0,3] opcodes for faster internal function calls have been
added. These opcodes don't create a stack frame, but pass arguments via opcode
operands. They only work for functions that are known at compile-time, and

View File

@ -17,7 +17,7 @@ pcntl_signal(SIGUSR1, function (): void {
$fiber = new Fiber(function (): void {
echo "Fiber start\n";
posix_kill(posix_getpid(), SIGUSR1);
time_nanosleep(1);
time_nanosleep(1, 0);
echo "Fiber end\n";
});
@ -30,8 +30,9 @@ Fiber start
Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %ssignal-async.php:%d
Stack trace:
#0 %ssignal-async.php(%d): Fiber::suspend()
#1 %ssignal-async.php(%d): {closure:%s:%d}(%d, Array)
#2 [internal function]: {closure:%s:%d}()
#3 %ssignal-async.php(%d): Fiber->start()
#4 {main}
#1 [internal function]: {closure:%s:%d}(%d, Array)
#2 %ssignal-async.php(%d): posix_kill(%d, %d)
#3 [internal function]: {closure:%s:%d}()
#4 %ssignal-async.php(%d): Fiber->start()
#5 {main}
thrown in %ssignal-async.php on line %d

View File

@ -341,7 +341,22 @@ extern ZEND_API size_t (*zend_printf)(const char *format, ...) ZEND_ATTRIBUTE_PT
extern ZEND_API zend_write_func_t zend_write;
extern ZEND_API FILE *(*zend_fopen)(zend_string *filename, zend_string **opened_path);
extern ZEND_API void (*zend_ticks_function)(int ticks);
/* Called by the VM in certain places like at the loop header, user function
* entry, and after internal function calls, if EG(vm_interrupt) has been set.
*
* If this is used to switch the EG(current_execute_data), such as implementing
* a coroutine scheduler, then it needs to check the top frame to see if it's
* an internal function. If an internal function is on top, then the frame
* shouldn't be switched away.
*
* Prior to PHP 8.0, this check was not necessary. In PHP 8.0,
* zend_call_function started calling zend_interrupt_function, and in 8.4 the
* DO_*CALL* opcodes started calling the zend_interrupt_function while the
* internal frame is still on top.
*/
extern ZEND_API void (*zend_interrupt_function)(zend_execute_data *execute_data);
extern ZEND_API void (*zend_error_cb)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message);
extern ZEND_API void (*zend_on_timeout)(int seconds);
extern ZEND_API zend_result (*zend_stream_open_function)(zend_file_handle *handle);

View File

@ -4095,6 +4095,16 @@ ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *exec
}
/* }}} */
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *call)
{
zend_atomic_bool_store_ex(&EG(vm_interrupt), false);
if (zend_atomic_bool_load_ex(&EG(timed_out))) {
zend_timeout();
} else if (zend_interrupt_function) {
zend_interrupt_function(call);
}
}
#define ZEND_VM_INTERRUPT_CHECK() do { \
if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \
ZEND_VM_INTERRUPT(); \
@ -4107,6 +4117,12 @@ ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *exec
} \
} while (0)
#define ZEND_VM_FCALL_INTERRUPT_CHECK(call) do { \
if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \
zend_fcall_interrupt(call); \
} \
} while (0)
/*
* Stack Frame Layout (the whole stack frame is allocated at once)
* ==================
@ -5541,9 +5557,12 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint
CHECK_SYMBOL_TABLES() \
OPLINE = new_op
#define ZEND_VM_SET_OPCODE(new_op) \
#define ZEND_VM_SET_OPCODE_NO_INTERRUPT(new_op) \
CHECK_SYMBOL_TABLES() \
OPLINE = new_op; \
OPLINE = new_op
#define ZEND_VM_SET_OPCODE(new_op) \
ZEND_VM_SET_OPCODE_NO_INTERRUPT(new_op); \
ZEND_VM_INTERRUPT_CHECK()
#define ZEND_VM_SET_RELATIVE_OPCODE(opline, offset) \

View File

@ -542,6 +542,11 @@ ZEND_COLD void zend_magic_get_property_type_inconsistency_error(const zend_prope
ZEND_COLD void zend_match_unhandled_error(const zval *value);
/* Call this to handle the timeout or the interrupt function. It will set
* EG(vm_interrupt) to false.
*/
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *call);
static zend_always_inline void *zend_get_bad_ptr(void)
{
ZEND_UNREACHABLE();

View File

@ -55,10 +55,11 @@ void zend_startup_system_id(void)
zend_system_id[0] = '\0';
}
#define ZEND_HOOK_AST_PROCESS (1 << 0)
#define ZEND_HOOK_COMPILE_FILE (1 << 1)
#define ZEND_HOOK_EXECUTE_EX (1 << 2)
#define ZEND_HOOK_EXECUTE_INTERNAL (1 << 3)
#define ZEND_HOOK_AST_PROCESS (1 << 0)
#define ZEND_HOOK_COMPILE_FILE (1 << 1)
#define ZEND_HOOK_EXECUTE_EX (1 << 2)
#define ZEND_HOOK_EXECUTE_INTERNAL (1 << 3)
#define ZEND_HOOK_INTERRUPT_FUNCTION (1 << 4)
void zend_finalize_system_id(void)
{
@ -77,6 +78,9 @@ void zend_finalize_system_id(void)
if (zend_execute_internal) {
hooks |= ZEND_HOOK_EXECUTE_INTERNAL;
}
if (zend_interrupt_function) {
hooks |= ZEND_HOOK_INTERRUPT_FUNCTION;
}
PHP_MD5Update(&context, &hooks, sizeof hooks);
for (int16_t i = 0; i < 256; i++) {

View File

@ -4084,6 +4084,7 @@ ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
}
#endif
ZEND_OBSERVER_FCALL_END(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
zend_vm_stack_free_args(call);
@ -4107,7 +4108,7 @@ ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -4205,6 +4206,7 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER))
}
#endif
ZEND_OBSERVER_FCALL_END(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
@ -4235,7 +4237,7 @@ ZEND_VM_C_LABEL(fcall_by_name_end):
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -4325,6 +4327,7 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
}
#endif
ZEND_OBSERVER_FCALL_END(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
@ -4353,8 +4356,7 @@ ZEND_VM_C_LABEL(fcall_end):
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}

36
Zend/zend_vm_execute.h generated
View File

@ -1299,6 +1299,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_RETV
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
zend_vm_stack_free_args(call);
@ -1321,7 +1323,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_RETV
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1361,6 +1363,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_RETV
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
zend_vm_stack_free_args(call);
@ -1383,7 +1387,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_RETV
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1424,6 +1428,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_OBS
}
#endif
zend_observer_fcall_end(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
zend_vm_stack_free_args(call);
@ -1447,7 +1452,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_ICALL_SPEC_OBS
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1591,6 +1596,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
goto fcall_by_name_end;
@ -1620,7 +1627,7 @@ fcall_by_name_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1691,6 +1698,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
goto fcall_by_name_end;
@ -1720,7 +1729,7 @@ fcall_by_name_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1793,6 +1802,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_
}
#endif
zend_observer_fcall_end(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
@ -1823,7 +1833,7 @@ fcall_by_name_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -1912,6 +1922,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
goto fcall_end;
@ -1939,8 +1951,7 @@ fcall_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -2029,6 +2040,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV
}
#endif
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
goto fcall_end;
@ -2056,8 +2069,7 @@ fcall_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}
@ -2147,6 +2159,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_OBS
}
#endif
zend_observer_fcall_end(call, EG(exception) ? NULL : ret);
ZEND_VM_FCALL_INTERRUPT_CHECK(call);
EG(current_execute_data) = execute_data;
@ -2175,8 +2188,7 @@ fcall_end:
zend_rethrow_exception(execute_data);
HANDLE_EXCEPTION();
}
ZEND_VM_SET_OPCODE(opline + 1);
ZEND_VM_SET_OPCODE_NO_INTERRUPT(opline + 1);
ZEND_VM_CONTINUE();
}

View File

@ -1425,9 +1425,7 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
zend_jit_set_last_valid_opline(&ctx, op_array->opcodes + ssa->cfg.blocks[b].start);
}
if (ssa->cfg.blocks[b].flags & ZEND_BB_LOOP_HEADER) {
if (!zend_jit_check_timeout(&ctx, op_array->opcodes + ssa->cfg.blocks[b].start, NULL)) {
goto jit_failure;
}
zend_jit_check_timeout(&ctx, op_array->opcodes + ssa->cfg.blocks[b].start, NULL);
}
if (!ssa->cfg.blocks[b].len) {
zend_jit_bb_end(&ctx, b);

View File

@ -1857,7 +1857,7 @@ static void jit_OBJ_RELEASE(zend_jit_ctx *jit, ir_ref ref)
ir_MERGE_list(end_inputs);
}
static int zend_jit_check_timeout(zend_jit_ctx *jit, const zend_op *opline, const void *exit_addr)
static void zend_jit_check_timeout(zend_jit_ctx *jit, const zend_op *opline, const void *exit_addr)
{
ir_ref ref = ir_LOAD_U8(jit_EG(vm_interrupt));
@ -1873,7 +1873,6 @@ static int zend_jit_check_timeout(zend_jit_ctx *jit, const zend_op *opline, cons
ir_IJMP(jit_STUB_ADDR(jit, jit_stub_interrupt_handler));
ir_IF_FALSE(if_timeout);
}
return 1;
}
/* stubs */
@ -2451,9 +2450,7 @@ static int zend_jit_trace_exit_stub(zend_jit_ctx *jit)
}
// check for interrupt (try to avoid this ???)
if (!zend_jit_check_timeout(jit, NULL, NULL)) {
return 0;
}
zend_jit_check_timeout(jit, NULL, NULL);
addr = zend_jit_orig_opline_handler(jit);
if (GCC_GLOBAL_REGS) {
@ -3078,6 +3075,7 @@ static void zend_jit_setup_disasm(void)
REGISTER_HELPER(zend_jit_pre_dec_obj_helper);
REGISTER_HELPER(zend_jit_post_dec_obj_helper);
REGISTER_HELPER(zend_jit_rope_end);
REGISTER_HELPER(zend_fcall_interrupt);
#ifndef ZTS
REGISTER_DATA(EG(current_execute_data));
@ -10199,6 +10197,19 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
jit_observer_fcall_end(jit, rx, res_ref);
}
/* When zend_interrupt_function is set, it gets called while
* the frame is still on top. This is less efficient than
* doing it later once it's popped off. There is code further
* down that handles when there isn't an interrupt function.
*/
if (zend_interrupt_function) {
// JIT: if (EG(vm_interrupt)) zend_fcall_interrupt(execute_data);
ir_ref if_interrupt = ir_IF(ir_LOAD_U8(jit_EG(vm_interrupt)));
ir_IF_TRUE_cold(if_interrupt);
ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_fcall_interrupt), rx);
ir_MERGE_WITH_EMPTY_FALSE(if_interrupt);
}
// JIT: EG(current_execute_data) = execute_data;
ir_STORE(jit_EG(current_execute_data), jit_FP(jit));
@ -10299,20 +10310,23 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
ir_GUARD_NOT(ir_LOAD_A(jit_EG_exception(jit)),
jit_STUB_ADDR(jit, jit_stub_icall_throw));
// TODO: Can we avoid checking for interrupts after each call ???
if (trace && jit->last_valid_opline != opline) {
int32_t exit_point = zend_jit_trace_get_exit_point(opline + 1, ZEND_JIT_EXIT_TO_VM);
/* If there isn't a zend_interrupt_function, the timeout is
* handled here because it's more efficient.
*/
if (!zend_interrupt_function) {
// TODO: Can we avoid checking for interrupts after each call ???
if (trace && jit->last_valid_opline != opline) {
int32_t exit_point = zend_jit_trace_get_exit_point(opline + 1, ZEND_JIT_EXIT_TO_VM);
exit_addr = zend_jit_trace_get_exit_addr(exit_point);
if (!exit_addr) {
return 0;
exit_addr = zend_jit_trace_get_exit_addr(exit_point);
if (!exit_addr) {
return 0;
}
} else {
exit_addr = NULL;
}
} else {
exit_addr = NULL;
}
if (!zend_jit_check_timeout(jit, opline + 1, exit_addr)) {
return 0;
zend_jit_check_timeout(jit, opline + 1, exit_addr);
}
if ((!trace || !func) && opline->opcode != ZEND_DO_ICALL) {
@ -10347,7 +10361,7 @@ static int zend_jit_constructor(zend_jit_ctx *jit, const zend_op *opline, const
}
}
/* override predecessors of the next block */
/* override predecessors of the next block */
ZEND_ASSERT(jit->ssa->cfg.blocks[next_block].predecessors_count == 1);
if (!jit->ctx.control) {
ZEND_ASSERT(jit->bb_edges[jit->bb_predecessors[next_block]]);