From fb5617f638a4469402199767b55e25a4fcf3f2e5 Mon Sep 17 00:00:00 2001 From: Alexandru Patranescu Date: Sat, 7 Mar 2026 11:38:14 +0200 Subject: [PATCH 1/2] Add consumed_args support for fcall arguments --- Zend/zend_API.c | 1 + Zend/zend_API.h | 1 + Zend/zend_closures.c | 1 + Zend/zend_execute_API.c | 13 ++++- ext/dom/xpath_callbacks.c | 1 + ext/ffi/ffi.c | 1 + ext/zend_test/test.c | 55 +++++++++++++++++++ ext/zend_test/test.stub.php | 2 + ext/zend_test/test_arginfo.h | 10 +++- ext/zend_test/test_decl.h | 8 +-- ext/zend_test/tests/consumed_args_basic.phpt | 32 +++++++++++ .../tests/consumed_args_mask_selectivity.phpt | 20 +++++++ .../tests/consumed_args_ref_arg.phpt | 35 ++++++++++++ .../consumed_args_ref_param_and_ref_arg.phpt | 37 +++++++++++++ .../tests/consumed_args_ref_required.phpt | 24 ++++++++ 15 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 ext/zend_test/tests/consumed_args_basic.phpt create mode 100644 ext/zend_test/tests/consumed_args_mask_selectivity.phpt create mode 100644 ext/zend_test/tests/consumed_args_ref_arg.phpt create mode 100644 ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt create mode 100644 ext/zend_test/tests/consumed_args_ref_required.phpt diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 89333d89af9df..bb6425dfc6656 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4238,6 +4238,7 @@ ZEND_API zend_result zend_fcall_info_init(zval *callable, uint32_t check_flags, fci->param_count = 0; fci->params = NULL; fci->named_params = NULL; + fci->consumed_args = 0; return SUCCESS; } diff --git a/Zend/zend_API.h b/Zend/zend_API.h index d78ee6604e34d..638e856ed9776 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -49,6 +49,7 @@ typedef struct _zend_fcall_info { zval *params; zend_object *object; uint32_t param_count; + uint32_t consumed_args; /* This hashtable can also contain positional arguments (with integer keys), * which will be appended to the normal params[]. This makes it easier to * integrate APIs like call_user_func_array(). The usual restriction that diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index cca69985a0dfe..40baefbd3f300 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -155,6 +155,7 @@ ZEND_METHOD(Closure, call) fci_cache.object = fci.object = newobj; fci.size = sizeof(fci); + fci.consumed_args = 0; ZVAL_OBJ(&fci.function_name, &closure->std); ZVAL_UNDEF(&closure_result); fci.retval = &closure_result; diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index dbd2a9039cfc9..18e14724af476 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -797,6 +797,7 @@ zend_result _call_user_function_impl(zval *object, zval *function_name, zval *re fci.param_count = param_count; fci.params = params; fci.named_params = named_params; + fci.consumed_args = 0; return zend_call_function(&fci, NULL); } @@ -905,7 +906,16 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ } if (EXPECTED(!must_wrap)) { - ZVAL_COPY(param, arg); + if (EXPECTED(fci->consumed_args == 0)) { + ZVAL_COPY(param, arg); + } else { + if (i < 32 && (fci->consumed_args & (1u << i)) && !Z_ISREF_P(arg) && arg == &fci->params[i]) { + ZVAL_COPY_VALUE(param, arg); + ZVAL_UNDEF(arg); + } else { + ZVAL_COPY(param, arg); + } + } } else { Z_TRY_ADDREF_P(arg); ZVAL_NEW_REF(param, arg); @@ -1091,6 +1101,7 @@ ZEND_API void zend_call_known_function( fci.param_count = param_count; fci.params = params; fci.named_params = named_params; + fci.consumed_args = 0; ZVAL_UNDEF(&fci.function_name); /* Unused */ fcic.function_handler = fn; diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c index 0974db475b3a9..70e85fc2a9191 100644 --- a/ext/dom/xpath_callbacks.c +++ b/ext/dom/xpath_callbacks.c @@ -408,6 +408,7 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat fci.param_count = param_count; fci.params = params; fci.named_params = NULL; + fci.consumed_args = 0; ZVAL_STRINGL(&fci.function_name, function_name, function_name_length); zend_call_function(&fci, NULL); diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c index d5b8eb59f0c90..b729af9695b16 100644 --- a/ext/ffi/ffi.c +++ b/ext/ffi/ffi.c @@ -950,6 +950,7 @@ static void zend_ffi_callback_trampoline(ffi_cif* cif, void* ret, void** args, v fci.object = NULL; fci.param_count = callback_data->arg_count; fci.named_params = NULL; + fci.consumed_args = 0; if (callback_data->type->func.args) { int n = 0; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 0faf65f36437f..ac69033a78b97 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -548,6 +548,61 @@ static ZEND_FUNCTION(zend_call_method_if_exists) } } +static ZEND_FUNCTION(zend_test_call_with_consumed_args) +{ + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fcc = empty_fcall_info_cache; + zval *args; + zend_long consumed_args; + zval retval; + uint32_t actual_consumed_args = 0; + uint32_t i; + zend_result call_result; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_FUNC(fci, fcc) + Z_PARAM_ARRAY(args) + Z_PARAM_LONG(consumed_args) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(consumed_args < 0 || consumed_args > UINT32_MAX)) { + zend_argument_value_error(3, "must be between 0 and 4294967295"); + RETURN_THROWS(); + } + + zend_fcall_info_args(&fci, args); + + ZVAL_UNDEF(&retval); + fci.retval = &retval; + fci.consumed_args = (uint32_t) consumed_args; + + call_result = zend_call_function(&fci, &fcc); + + for (i = 0; i < fci.param_count && i < 32; i++) { + if (Z_ISUNDEF(fci.params[i])) { + actual_consumed_args |= (1u << i); + } + } + + zend_fcall_info_args_clear(&fci, true); + + if (call_result == FAILURE || EG(exception)) { + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + RETURN_THROWS(); + } + + array_init(return_value); + add_assoc_long(return_value, "consumed_args", actual_consumed_args); + + if (Z_ISUNDEF(retval)) { + add_assoc_null(return_value, "retval"); + } else { + add_assoc_zval(return_value, "retval", &retval); + } +} + static ZEND_FUNCTION(zend_get_unit_enum) { ZEND_PARSE_PARAMETERS_NONE(); diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 63f9650f7ef61..b8876c0415e46 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -303,6 +303,8 @@ function zend_object_init_with_constructor(string $class, mixed ...$args): mixed function zend_call_method_if_exists(object $obj, string $method, mixed ...$args): mixed {} + function zend_test_call_with_consumed_args(callable $cb, array $args, int $consumed_args): array {} + function zend_test_zend_ini_parse_quantity(string $str): int {} function zend_test_zend_ini_parse_uquantity(string $str): int {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 6b5dfe9c2567e..725cb74e1d1d8 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 test.stub.php instead. - * Stub hash: 0dc403dd439157aa09ae0692b295092bdc59c1d0 + * Stub hash: 25fbbae598c6be223bee03428591d5a33075fe1a * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, IS_NEVER, 0) @@ -123,6 +123,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_call_method_if_exists, 0, 2 ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_call_with_consumed_args, 0, 3, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, cb, IS_CALLABLE, 0) + ZEND_ARG_TYPE_INFO(0, args, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, consumed_args, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_zend_ini_parse_quantity, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -316,6 +322,7 @@ static ZEND_FUNCTION(zend_get_current_func_name); static ZEND_FUNCTION(zend_call_method); static ZEND_FUNCTION(zend_object_init_with_constructor); static ZEND_FUNCTION(zend_call_method_if_exists); +static ZEND_FUNCTION(zend_test_call_with_consumed_args); static ZEND_FUNCTION(zend_test_zend_ini_parse_quantity); static ZEND_FUNCTION(zend_test_zend_ini_parse_uquantity); static ZEND_FUNCTION(zend_test_zend_ini_str); @@ -448,6 +455,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_call_method, arginfo_zend_call_method) ZEND_FE(zend_object_init_with_constructor, arginfo_zend_object_init_with_constructor) ZEND_FE(zend_call_method_if_exists, arginfo_zend_call_method_if_exists) + ZEND_FE(zend_test_call_with_consumed_args, arginfo_zend_test_call_with_consumed_args) ZEND_FE(zend_test_zend_ini_parse_quantity, arginfo_zend_test_zend_ini_parse_quantity) ZEND_FE(zend_test_zend_ini_parse_uquantity, arginfo_zend_test_zend_ini_parse_uquantity) ZEND_FE(zend_test_zend_ini_str, arginfo_zend_test_zend_ini_str) diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index a6254865a87a5..1bb2878e2212d 100644 --- a/ext/zend_test/test_decl.h +++ b/ext/zend_test/test_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 0dc403dd439157aa09ae0692b295092bdc59c1d0 */ + * Stub hash: 25fbbae598c6be223bee03428591d5a33075fe1a */ -#ifndef ZEND_TEST_DECL_0dc403dd439157aa09ae0692b295092bdc59c1d0_H -#define ZEND_TEST_DECL_0dc403dd439157aa09ae0692b295092bdc59c1d0_H +#ifndef ZEND_TEST_DECL_25fbbae598c6be223bee03428591d5a33075fe1a_H +#define ZEND_TEST_DECL_25fbbae598c6be223bee03428591d5a33075fe1a_H typedef enum zend_enum_ZendTestUnitEnum { ZEND_ENUM_ZendTestUnitEnum_Foo = 1, @@ -27,4 +27,4 @@ typedef enum zend_enum_ZendTestEnumWithInterface { ZEND_ENUM_ZendTestEnumWithInterface_Bar = 2, } zend_enum_ZendTestEnumWithInterface; -#endif /* ZEND_TEST_DECL_0dc403dd439157aa09ae0692b295092bdc59c1d0_H */ +#endif /* ZEND_TEST_DECL_25fbbae598c6be223bee03428591d5a33075fe1a_H */ diff --git a/ext/zend_test/tests/consumed_args_basic.phpt b/ext/zend_test/tests/consumed_args_basic.phpt new file mode 100644 index 0000000000000..2b051bc822d01 --- /dev/null +++ b/ext/zend_test/tests/consumed_args_basic.phpt @@ -0,0 +1,32 @@ +--TEST-- +zend_test_call_with_consumed_args(): consume a non-reference arg +--EXTENSIONS-- +zend_test +--FILE-- + [$a, $b], + [[1, 2, 3], "x"], + 1, +); + +var_dump($result["consumed_args"]); +var_dump($result["retval"]); + +?> +--EXPECT-- +int(1) +array(2) { + [0]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } + [1]=> + string(1) "x" +} diff --git a/ext/zend_test/tests/consumed_args_mask_selectivity.phpt b/ext/zend_test/tests/consumed_args_mask_selectivity.phpt new file mode 100644 index 0000000000000..051fd38cb7dc3 --- /dev/null +++ b/ext/zend_test/tests/consumed_args_mask_selectivity.phpt @@ -0,0 +1,20 @@ +--TEST-- +zend_test_call_with_consumed_args(): consume mask applies selectively +--EXTENSIONS-- +zend_test +--FILE-- + null, + [[1], [2], [3]], + 5, +); + +var_dump($result["consumed_args"]); +var_dump($result["retval"]); + +?> +--EXPECT-- +int(5) +NULL diff --git a/ext/zend_test/tests/consumed_args_ref_arg.phpt b/ext/zend_test/tests/consumed_args_ref_arg.phpt new file mode 100644 index 0000000000000..a528860006b08 --- /dev/null +++ b/ext/zend_test/tests/consumed_args_ref_arg.phpt @@ -0,0 +1,35 @@ +--TEST-- +zend_test_call_with_consumed_args(): do not consume reference args +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECT-- +int(0) +array(2) { + [0]=> + int(10) + [1]=> + int(20) +} +array(1) { + [0]=> + int(10) +} diff --git a/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt b/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt new file mode 100644 index 0000000000000..01d4025842a41 --- /dev/null +++ b/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt @@ -0,0 +1,37 @@ +--TEST-- +zend_test_call_with_consumed_args(): ref-required params with reference args are not consumed +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECT-- +int(0) +array(2) { + [0]=> + int(10) + [1]=> + int(20) +} +array(2) { + [0]=> + int(10) + [1]=> + int(20) +} diff --git a/ext/zend_test/tests/consumed_args_ref_required.phpt b/ext/zend_test/tests/consumed_args_ref_required.phpt new file mode 100644 index 0000000000000..f261131a686ab --- /dev/null +++ b/ext/zend_test/tests/consumed_args_ref_required.phpt @@ -0,0 +1,24 @@ +--TEST-- +zend_test_call_with_consumed_args(): ref-required params are not consumed +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECTF-- +Warning: {closure:%s:%d}(): Argument #1 ($a) must be passed by reference, value given in %s on line %d +int(0) +int(42) From 91716819e52ee88dd7ca0f714deeefd57340bd81 Mon Sep 17 00:00:00 2001 From: Alexandru Patranescu Date: Sat, 7 Mar 2026 11:38:39 +0200 Subject: [PATCH 2/2] Use consumed_args for array_reduce and some other callback-using functions --- Zend/zend_API.h | 1 + ext/pcre/php_pcre.c | 1 + ext/standard/array.c | 1 + main/output.c | 1 + 4 files changed, 4 insertions(+) diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 638e856ed9776..0409bb2141cd5 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -340,6 +340,7 @@ typedef struct _zend_fcall_info_cache { #define ZEND_FCI_INITIALIZED(fci) ((fci).size != 0) #define ZEND_FCC_INITIALIZED(fcc) ((fcc).function_handler != NULL) +#define ZEND_FCI_CONSUMED_ARG(arg_num) (1u << ((arg_num) - 1)) ZEND_API int zend_next_free_module(void); diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 32ab6b0e5090b..77b7aff0ea6eb 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -1570,6 +1570,7 @@ static zend_string *preg_do_repl_func(zend_fcall_info *fci, zend_fcall_info_cach fci->retval = &retval; fci->param_count = 1; fci->params = &arg; + fci->consumed_args = ZEND_FCI_CONSUMED_ARG(1); zend_call_function(fci, fcc); zval_ptr_dtor(&arg); if (EXPECTED(Z_TYPE(retval) == IS_STRING)) { diff --git a/ext/standard/array.c b/ext/standard/array.c index de889154edd42..9a2043e94e3fc 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6409,6 +6409,7 @@ PHP_FUNCTION(array_reduce) fci.retval = return_value; fci.param_count = 2; fci.params = args; + fci.consumed_args = ZEND_FCI_CONSUMED_ARG(1); ZEND_HASH_FOREACH_VAL(htbl, operand) { ZVAL_COPY_VALUE(&args[0], return_value); diff --git a/main/output.c b/main/output.c index ff65a0f9a4d8e..d3dd4e29c8968 100644 --- a/main/output.c +++ b/main/output.c @@ -972,6 +972,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl handler->func.user->fci.param_count = 2; handler->func.user->fci.params = ob_args; handler->func.user->fci.retval = &retval; + handler->func.user->fci.consumed_args = ZEND_FCI_CONSUMED_ARG(1); if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) { if (handler->flags & PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT) {