Commit 417327a6968 for php
commit 417327a6968ecf4cb2c0c6b4c4bc0d394cbc4a76
Author: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Date: Tue Sep 29 12:01:31 2026 +0200
Fix OSS-Fuzz #552682112: assertion failure wrt zp_arg_must_be_sent_by_ref() (#23760)
Runtime rejects sending PFA args non-variable in a by-ref position.
The const expression path needs a similar check.
diff --git a/NEWS b/NEWS
index 8e6014f1140..73b488bd4ac 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,8 @@ PHP NEWS
function). (ndossche)
. Fixed AVX being reported as supported when the OS has not enabled AVX
state. (Ilia Alshanetsky)
+ . Fixed OSS-Fuzz #552682112 (assertion failure wrt
+ zp_arg_must_be_sent_by_ref()). (ndossche)
- FFI:
. Fixed crashes with FFI callbacks created from __call() trampolines
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 8bbdc5caabe..255d4c2ca89 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -215,6 +215,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES
. Added zend_argument_error_ex(), zend_argument_type_error_ex(),
zend_argument_value_error_ex().
. Added zend_ast_dup().
+ . Added zend_cannot_pass_by_reference_ex().
. Added zend_compile_ast().
. Added zend_check_type_ex().
. Added zend_create_partial_closure().
diff --git a/Zend/tests/partial_application/constexpr_016.phpt b/Zend/tests/partial_application/constexpr_016.phpt
new file mode 100644
index 00000000000..035831e07ff
--- /dev/null
+++ b/Zend/tests/partial_application/constexpr_016.phpt
@@ -0,0 +1,43 @@
+--TEST--
+PFA in constexpr: binding a by-reference parameter
+--FILE--
+<?php
+
+function byRef($a, &$b) {}
+function byRefVariadic(&...$args) {}
+
+class C {
+ public static function staticByRef($a, &$b) {}
+}
+
+function f1($x = byRef(new stdClass, new stdClass, ...)) {}
+function f2($x = byRef(b: new stdClass, a: ?)) {}
+function f3($x = byRefVariadic(new stdClass, ...)) {}
+function f4($x = byRefVariadic(extra: new stdClass, ...)) {}
+function f5($x = C::staticByRef(new stdClass, new stdClass, ...)) {}
+
+foreach (['f1', 'f2', 'f3', 'f4', 'f5'] as $f) {
+ try {
+ $f();
+ } catch (Error $e) {
+ echo get_class($e), ": ", $e->getMessage(), "\n";
+ }
+}
+
+function f6($x = byRef(new stdClass, ?)) {
+ return $x;
+}
+
+$partial = f6();
+$var = 1;
+var_dump($partial instanceof Closure);
+$partial($var);
+
+?>
+--EXPECT--
+Error: byRef(): Argument #2 ($b) could not be passed by reference
+Error: byRef(): Argument #2 ($b) could not be passed by reference
+Error: byRefVariadic(): Argument #1 could not be passed by reference
+Error: byRefVariadic(): Argument #1 could not be passed by reference
+Error: C::staticByRef(): Argument #2 ($b) could not be passed by reference
+bool(true)
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 6a71fc5aeca..b81296599d6 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -714,7 +714,8 @@ static zend_execute_data *zend_ast_evaluate_arg_list(
arg = ZEND_CALL_VAR_NUM(frame, ZEND_CALL_NUM_ARGS(frame));
}
- if (arg_ast->kind == ZEND_AST_PLACEHOLDER_ARG) {
+ bool is_placeholder = arg_ast->kind == ZEND_AST_PLACEHOLDER_ARG;
+ if (is_placeholder) {
if (arg_ast->attr == ZEND_PLACEHOLDER_VARIADIC) {
if (uses_variadic_placeholder) {
*uses_variadic_placeholder = true;
@@ -732,6 +733,12 @@ static zend_execute_data *zend_ast_evaluate_arg_list(
if (!arg_name) {
ZEND_CALL_NUM_ARGS(frame)++;
}
+
+ /* A constant expression can't be bound to a reference because it ain't a CV. */
+ if (!is_placeholder && UNEXPECTED(ARG_MUST_BE_SENT_BY_REF(func, arg_num))) {
+ zend_cannot_pass_by_reference_ex(func, arg_num);
+ goto fail;
+ }
}
return frame;
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 799475d7df9..a5f9d1e8c84 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -624,11 +624,10 @@ static zend_never_inline ZEND_COLD zval *zend_wrong_assign_to_variable_reference
return zend_assign_to_variable_ex(variable_ptr, value_ptr, IS_TMP_VAR, EX_USES_STRICT_TYPES(), garbage_ptr);
}
-ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num)
+ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference_ex(const zend_function *func, uint32_t arg_num)
{
- const zend_execute_data *execute_data = EG(current_execute_data);
- zend_string *func_name = get_function_or_method_name(EX(call)->func);
- const char *param_name = get_function_arg_name(EX(call)->func, arg_num);
+ zend_string *func_name = get_function_or_method_name(func);
+ const char *param_name = get_function_arg_name(func, arg_num);
zend_throw_error(NULL, "%s(): Argument #%d%s%s%s could not be passed by reference",
ZSTR_VAL(func_name), arg_num, param_name ? " ($" : "", param_name ? param_name : "", param_name ? ")" : ""
@@ -637,6 +636,12 @@ ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_refe
zend_string_release(func_name);
}
+ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num)
+{
+ const zend_execute_data *execute_data = EG(current_execute_data);
+ zend_cannot_pass_by_reference_ex(EX(call)->func, arg_num);
+}
+
static zend_never_inline ZEND_COLD void zend_throw_auto_init_in_prop_error(const zend_property_info *prop) {
zend_string *type_str = zend_type_to_string(prop->type);
zend_type_error(
diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index 2250a873af2..14c1e6701f0 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -522,6 +522,7 @@ ZEND_API uint32_t zend_get_executed_lineno(void);
ZEND_API zend_class_entry *zend_get_executed_scope(void);
ZEND_API bool zend_is_executing(void);
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num);
+ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference_ex(const zend_function *func, uint32_t arg_num);
ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals);
ZEND_API void zend_unset_timeout(void);