Commit 8fc9e7c8e8f for php

commit 8fc9e7c8e8f771ab55671cec00c4e468ec511efe
Author: Bob Weinand <bobwei9@hotmail.com>
Date:   Fri Oct 30 15:34:17 2026 +0100

    Fix assertion in VM when taking ref-returning fcall by-value

    zend_return_unwrap_ref() is expected to be called now, but internal functions returning by reference were failing the debug assertion, expecting that they would unconditionally return by reference.

    Adjusted the assertion.

    Signed-off-by: Bob Weinand <bobwei9@hotmail.com>

diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 3133caea2e0..e929bc41a03 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -6004,26 +6004,7 @@ ZEND_API zval *zend_get_zval_ptr(const zend_op *opline, int op_type, const znode

 ZEND_API void zend_return_unwrap_ref(zend_execute_data *execute_data, zval *return_value)
 {
-	if (!return_value || !Z_ISREF_P(return_value)) {
-		return;
-	}
-
-	zend_execute_data *prev_ex = EX(prev_execute_data);
-	if (!prev_ex || !prev_ex->func || !ZEND_USER_CODE(prev_ex->func->type)) {
-		return;
+	if (return_value && Z_ISREF_P(return_value) && zend_return_should_unwrap_ref(execute_data)) {
+		zend_unwrap_reference(return_value);
 	}
-
-	const zend_op *do_opline = prev_ex->opline;
-	if (do_opline->result_type != IS_TMP_VAR) {
-		return;
-	}
-
-	if (do_opline->opcode != ZEND_DO_FCALL
-	 && do_opline->opcode != ZEND_DO_FCALL_BY_NAME
-	 && do_opline->opcode != ZEND_DO_ICALL
-	 && do_opline->opcode != ZEND_DO_UCALL) {
-		return;
-	}
-
-	zend_unwrap_reference(return_value);
 }
diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index 14c1e6701f0..bdb4f66cb2f 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -676,6 +676,24 @@ static zend_always_inline void *zend_get_bad_ptr(void)
 	return NULL;
 }

+static zend_always_inline bool zend_return_should_unwrap_ref(const zend_execute_data *call)
+{
+	const zend_execute_data *prev_ex = call->prev_execute_data;
+	if (!prev_ex || !prev_ex->func || !ZEND_USER_CODE(prev_ex->func->type)) {
+		return false;
+	}
+
+	const zend_op *do_opline = prev_ex->opline;
+	if (do_opline->result_type != IS_TMP_VAR) {
+		return false;
+	}
+
+	return do_opline->opcode == ZEND_DO_FCALL
+		|| do_opline->opcode == ZEND_DO_FCALL_BY_NAME
+		|| do_opline->opcode == ZEND_DO_ICALL
+		|| do_opline->opcode == ZEND_DO_UCALL;
+}
+
 ZEND_API void zend_return_unwrap_ref(zend_execute_data *call, zval *return_value);

 END_EXTERN_C()
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index b9e992ff8a0..2c361bb4950 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -1106,7 +1106,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
 				bool result = zend_verify_internal_return_type(call->func, fci->retval);
 				ZEND_ASSERT(result);
 			}
-			ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
+			ZEND_ASSERT(((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) && !zend_return_should_unwrap_ref(call))
 				? Z_ISREF_P(fci->retval) : !Z_ISREF_P(fci->retval));
 			ZEND_ASSERT(!(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
 		}
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 4375b7ad3f3..b3d0597374c 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -4158,7 +4158,7 @@ ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
 			bool result = zend_verify_internal_return_type(call->func, ret);
 			ZEND_ASSERT(result);
 		}
-		ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
+		ZEND_ASSERT(((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) && !zend_return_should_unwrap_ref(call))
 			? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
 		zend_verify_internal_func_info(call->func, ret);
 		ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
@@ -4291,7 +4291,7 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER))
 				bool result = zend_verify_internal_return_type(call->func, ret);
 				ZEND_ASSERT(result);
 			}
-			ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
+			ZEND_ASSERT(((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) && !zend_return_should_unwrap_ref(call))
 				? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
 			zend_verify_internal_func_info(call->func, ret);
 			ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
@@ -4424,7 +4424,7 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
 				bool result = zend_verify_internal_return_type(call->func, ret);
 				ZEND_ASSERT(result);
 			}
-			ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
+			ZEND_ASSERT(((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) && !zend_return_should_unwrap_ref(call))
 				? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
 			zend_verify_internal_func_info(call->func, ret);
 			ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
@@ -9182,7 +9182,7 @@ ZEND_VM_HANDLER(158, ZEND_CALL_TRAMPOLINE, ANY, ANY, SPEC(OBSERVER))
 				bool result = zend_verify_internal_return_type(call->func, ret);
 				ZEND_ASSERT(result);
 			}
-			ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
+			ZEND_ASSERT(((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) && !zend_return_should_unwrap_ref(call))
 				? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
 			zend_verify_internal_func_info(call->func, ret);
 			ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 59891e70d9a..74ed4301047 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index c1ede5c6043..02f09a5a7ab 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -132,6 +132,18 @@ static void pass2(zend_script *script, void *context)
 	php_printf("pass2\n");
 }

+static ZEND_FUNCTION(zend_test_forward_reference)
+{
+	zval *value;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ZVAL(value)
+	ZEND_PARSE_PARAMETERS_END();
+
+	ZVAL_COPY(return_value, value);
+	zend_return_unwrap_ref(execute_data, return_value);
+}
+
 static ZEND_FUNCTION(zend_test_deprecated)
 {
 	zval *arg1;
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 8137b911a35..8719c6935a3 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -265,6 +265,8 @@ function zend_test_nullable_array_return(): null|array {}

     function zend_test_void_return(): void {}

+    function &zend_test_forward_reference(mixed &$value): mixed {}
+
     function zend_test_compile_string(string $source_string, string $filename, int $position): void {}

     /** @deprecated */
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index 184bcdbae14..d6fa8896c42 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h
index cab05e40a40..35e929d1a30 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index d58dc5d2622..9bb20c85e95 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ