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);