diff options
| author | jvoisin | 2026-05-10 01:01:43 +0200 |
|---|---|---|
| committer | jvoisin | 2026-05-10 01:01:43 +0200 |
| commit | 5f754c32df4bd8643d62babf6f805defe59c8c92 (patch) | |
| tree | 6fdc236f5ded2320ba004118e6fcbf43ea84c91c | |
| parent | 0cc8e708ebbff02abe7e3c2d0dc0660585ccd98c (diff) | |
Prevent opcache from inlining functions with return-value rules on PHP 8.5+optim85
PHP 8.5's opcache optimizer can inline trivial user functions (constant
return values), completely eliminating the DO_UCALL opcode. When this
happens, zend_execute_ex is never invoked and snuffleupagus's
return-value monitoring hooks never fire.
Fix this by setting ZEND_ACC_HAS_TYPE_HINTS on monitored functions'
op_arrays during compilation (via sp_op_array_handler). This flag is
checked by opcache's zend_try_inline_call() and prevents inlining.
For 0-arg functions — the only ones eligible for inlining — there are
no RECV opcodes, so the runtime impact is zero.
To enable sp_op_array_handler when return-value rules are configured,
the extension now registers itself as a zend extension and sets
ZEND_COMPILE_HANDLE_OP_ARRAY (previously only done for global_strict).
The disabled_function_echo_2 test is updated to use separate echo
statements and opcache.optimization_level=0, since opcache's echo
merging is a compile-time string concatenation that cannot be prevented
per-function.
This is a bit ugly, but it's the less awful solution to be able to hook return
values.
| -rw-r--r-- | src/snuffleupagus.c | 50 | ||||
| -rw-r--r-- | src/tests/disable_function/disabled_function_echo_2.phpt | 2 |
2 files changed, 49 insertions, 3 deletions
diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index 4d5fa09..ca7e6d4 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c | |||
| @@ -25,6 +25,39 @@ static inline void sp_op_array_handler(zend_op_array *const op) { | |||
| 25 | op->fn_flags |= ZEND_ACC_STRICT_TYPES; | 25 | op->fn_flags |= ZEND_ACC_STRICT_TYPES; |
| 26 | } | 26 | } |
| 27 | } | 27 | } |
| 28 | #if PHP_VERSION_ID >= 80500 | ||
| 29 | /* Prevent opcache from inlining user functions that have return-value | ||
| 30 | * monitoring rules, otherwise zend_execute_ex is never called and the | ||
| 31 | * hook never fires. ZEND_ACC_HAS_TYPE_HINTS is checked by | ||
| 32 | * zend_try_inline_call() and blocks inlining. For functions without | ||
| 33 | * actual type hints the only runtime effect is that ZEND_RECV opcodes | ||
| 34 | * are executed instead of skipped; for 0-arg functions (the common | ||
| 35 | * inlineable case) there are no RECV opcodes so the impact is zero. */ | ||
| 36 | if (op->function_name && ZEND_USER_CODE(op->type) && | ||
| 37 | ((SPCFG(disabled_functions_ret) && zend_hash_num_elements(SPCFG(disabled_functions_ret))) || | ||
| 38 | SPCFG(disabled_functions_reg_ret).disabled_functions)) { | ||
| 39 | char *fname = NULL; | ||
| 40 | if (op->scope) { | ||
| 41 | const size_t len = ZSTR_LEN(op->scope->name) + 2 + ZSTR_LEN(op->function_name) + 1; | ||
| 42 | fname = emalloc(len); | ||
| 43 | snprintf(fname, len, "%s::%s", ZSTR_VAL(op->scope->name), ZSTR_VAL(op->function_name)); | ||
| 44 | } else { | ||
| 45 | fname = estrdup(ZSTR_VAL(op->function_name)); | ||
| 46 | } | ||
| 47 | bool has_ret_rule = false; | ||
| 48 | if (SPCFG(disabled_functions_ret) && | ||
| 49 | zend_hash_str_find_ptr(SPCFG(disabled_functions_ret), fname, strlen(fname))) { | ||
| 50 | has_ret_rule = true; | ||
| 51 | } | ||
| 52 | if (!has_ret_rule && SPCFG(disabled_functions_reg_ret).disabled_functions) { | ||
| 53 | has_ret_rule = true; /* regex rules require runtime matching */ | ||
| 54 | } | ||
| 55 | if (has_ret_rule) { | ||
| 56 | op->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; | ||
| 57 | } | ||
| 58 | efree(fname); | ||
| 59 | } | ||
| 60 | #endif | ||
| 28 | } | 61 | } |
| 29 | 62 | ||
| 30 | ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) | 63 | ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) |
| @@ -582,12 +615,25 @@ static PHP_INI_MH(OnUpdateConfiguration) { | |||
| 582 | 615 | ||
| 583 | sp_hook_register_server_variables(); | 616 | sp_hook_register_server_variables(); |
| 584 | 617 | ||
| 585 | if (SPCFG(global_strict).enable) { | 618 | bool need_op_array_handler = SPCFG(global_strict).enable; |
| 619 | |||
| 620 | #if PHP_VERSION_ID >= 80500 | ||
| 621 | /* Register as zend extension to get op_array_handler callbacks, which we | ||
| 622 | * use to prevent opcache from inlining monitored functions. */ | ||
| 623 | if (SPCFG(disabled_functions_ret) && zend_hash_num_elements(SPCFG(disabled_functions_ret))) { | ||
| 624 | need_op_array_handler = true; | ||
| 625 | } | ||
| 626 | if (SPCFG(disabled_functions_reg_ret).disabled_functions) { | ||
| 627 | need_op_array_handler = true; | ||
| 628 | } | ||
| 629 | #endif | ||
| 630 | |||
| 631 | if (need_op_array_handler) { | ||
| 586 | if (!zend_get_extension(PHP_SNUFFLEUPAGUS_EXTNAME)) { | 632 | if (!zend_get_extension(PHP_SNUFFLEUPAGUS_EXTNAME)) { |
| 587 | zend_extension_entry.startup = NULL; | 633 | zend_extension_entry.startup = NULL; |
| 588 | zend_register_extension(&zend_extension_entry, NULL); | 634 | zend_register_extension(&zend_extension_entry, NULL); |
| 589 | } | 635 | } |
| 590 | // This is needed to implement the global strict mode | 636 | // This is needed to enable the op_array_handler callback |
| 591 | CG(compiler_options) |= ZEND_COMPILE_HANDLE_OP_ARRAY; | 637 | CG(compiler_options) |= ZEND_COMPILE_HANDLE_OP_ARRAY; |
| 592 | } | 638 | } |
| 593 | 639 | ||
diff --git a/src/tests/disable_function/disabled_function_echo_2.phpt b/src/tests/disable_function/disabled_function_echo_2.phpt index e519ec4..ce3488e 100644 --- a/src/tests/disable_function/disabled_function_echo_2.phpt +++ b/src/tests/disable_function/disabled_function_echo_2.phpt | |||
| @@ -2,9 +2,9 @@ | |||
| 2 | Echo hooking | 2 | Echo hooking |
| 3 | --SKIPIF-- | 3 | --SKIPIF-- |
| 4 | <?php if (!extension_loaded("snuffleupagus")) print "skip"; ?> | 4 | <?php if (!extension_loaded("snuffleupagus")) print "skip"; ?> |
| 5 | <?php if (PHP_VERSION_ID >= 80500) print "skip"; ?> | ||
| 6 | --INI-- | 5 | --INI-- |
| 7 | sp.configuration_file={PWD}/config/disabled_function_echo.ini | 6 | sp.configuration_file={PWD}/config/disabled_function_echo.ini |
| 7 | opcache.optimization_level=0 | ||
| 8 | --FILE-- | 8 | --FILE-- |
| 9 | <?php | 9 | <?php |
| 10 | echo "qwe"; | 10 | echo "qwe"; |
