Commit 1805822555e for php.net

commit 1805822555e38f6b75eb74be7c8b6856e3eb3d10
Author: Gina Peter Banyard <girgias@php.net>
Date:   Sun Jul 26 12:42:35 2026 +0100

    sapi: only use FCC for header_register_callback() (#22877)

    No need to rederive callability of zval, or copies of it.

diff --git a/main/SAPI.c b/main/SAPI.c
index 58e85539d54..523759ea79b 100644
--- a/main/SAPI.c
+++ b/main/SAPI.c
@@ -111,51 +111,25 @@ PHP_FUNCTION(header_register_callback)
 	zend_fcall_info fci;
 	zend_fcall_info_cache fcc;

-	if (zend_parse_parameters(ZEND_NUM_ARGS(), "f", &fci, &fcc) == FAILURE) {
+	if (zend_parse_parameters(ZEND_NUM_ARGS(), "F", &fci, &fcc) == FAILURE) {
 		RETURN_THROWS();
 	}

-	if (Z_TYPE(SG(callback_func)) != IS_UNDEF) {
-		zval_ptr_dtor(&SG(callback_func));
-		SG(fci_cache) = empty_fcall_info_cache;
+	if (ZEND_FCC_INITIALIZED(SG(send_header_fcc))) {
+		zend_fcc_dtor(&SG(send_header_fcc));
 	}

 	/* Don't store callback if headers have already been sent:
 	 * It won't get used and we won't have a chance to release it. */
-	if (!SG(headers_sent)) {
-		ZVAL_COPY(&SG(callback_func), &fci.function_name);
+	if (UNEXPECTED(SG(headers_sent))) {
+		zend_release_fcall_info_cache(&fcc);
+	} else {
+		zend_fcc_dup(&SG(send_header_fcc), &fcc);
 	}
-
 	RETURN_TRUE;
 }
 /* }}} */

-static void sapi_run_header_callback(zval *callback)
-{
-	int   error;
-	zend_fcall_info fci;
-	char *callback_error = NULL;
-	zval retval;
-
-	if (zend_fcall_info_init(callback, 0, &fci, &SG(fci_cache), NULL, &callback_error) == SUCCESS) {
-		fci.retval = &retval;
-
-		error = zend_call_function(&fci, &SG(fci_cache));
-		if (error == FAILURE) {
-			goto callback_failed;
-		} else {
-			zval_ptr_dtor(&retval);
-		}
-	} else {
-callback_failed:
-		php_error_docref(NULL, E_WARNING, "Could not call the sapi_header_callback");
-	}
-
-	if (callback_error) {
-		efree(callback_error);
-	}
-}
-
 SAPI_API void sapi_handle_post(void *arg)
 {
 	if (SG(request_info).post_entry && SG(request_info).content_type_dup) {
@@ -436,7 +410,6 @@ SAPI_API void sapi_activate(void)
 	SG(sapi_headers).http_status_line = NULL;
 	SG(sapi_headers).mimetype = NULL;
 	SG(headers_sent) = 0;
-	ZVAL_UNDEF(&SG(callback_func));
 	SG(read_post_bytes) = 0;
 	SG(request_info).request_body = NULL;
 	SG(request_info).current_user = NULL;
@@ -446,6 +419,7 @@ SAPI_API void sapi_activate(void)
 	SG(request_info).proto_num = 1000; /* Default to HTTP 1.0 */
 	SG(global_request_time) = 0;
 	SG(post_read) = 0;
+	SG(send_header_fcc) = empty_fcall_info_cache;
 	/* It's possible to override this general case in the activate() callback, if necessary. */
 	if (SG(request_info).request_method && !strcmp(SG(request_info).request_method, "HEAD")) {
 		SG(request_info).headers_only = 1;
@@ -889,12 +863,12 @@ SAPI_API int sapi_send_headers(void)
 		SG(sapi_headers).send_default_content_type = 0;
 	}

-	if (Z_TYPE(SG(callback_func)) != IS_UNDEF) {
-		zval cb;
-		ZVAL_COPY_VALUE(&cb, &SG(callback_func));
-		ZVAL_UNDEF(&SG(callback_func));
-		sapi_run_header_callback(&cb);
-		zval_ptr_dtor(&cb);
+	if (ZEND_FCC_INITIALIZED(SG(send_header_fcc))) {
+		zend_fcall_info_cache fcc = SG(send_header_fcc);
+		/* Prevent triggering the callback multiple times */
+		SG(send_header_fcc) = empty_fcall_info_cache;
+		zend_call_known_fcc(&fcc, NULL, 0, NULL, NULL);
+		zend_fcc_dtor(&fcc);
 	}

 	SG(headers_sent) = 1;
diff --git a/main/SAPI.h b/main/SAPI.h
index e62f686603c..d34c12f1bbc 100644
--- a/main/SAPI.h
+++ b/main/SAPI.h
@@ -143,8 +143,7 @@ typedef struct _sapi_globals_struct {
 	bool sapi_started;
 	double global_request_time;
 	HashTable known_post_content_types;
-	zval callback_func;
-	zend_fcall_info_cache fci_cache;
+	zend_fcall_info_cache send_header_fcc;
 	sapi_request_parse_body_context request_parse_body_context;
 } sapi_globals_struct;

diff --git a/tests/basic/header_register_callback_trampoline.phpt b/tests/basic/header_register_callback_trampoline.phpt
new file mode 100644
index 00000000000..cc4a1e4014e
--- /dev/null
+++ b/tests/basic/header_register_callback_trampoline.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test header_register_callback
+--FILE--
+<?php
+class TrampolineTest {
+    public function __call(string $name, array $arguments) {
+        echo 'Trampoline for ', $name, PHP_EOL;
+    }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'trampoline'];
+
+header_register_callback($callback);
+?>
+--EXPECT--
+Trampoline for trampoline
diff --git a/tests/basic/header_register_callback_trampoline_after_headers_sent.phpt b/tests/basic/header_register_callback_trampoline_after_headers_sent.phpt
new file mode 100644
index 00000000000..7f343c6d813
--- /dev/null
+++ b/tests/basic/header_register_callback_trampoline_after_headers_sent.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test header_register_callback
+--FILE--
+<?php
+class TrampolineTest {
+    public function __call(string $name, array $arguments) {
+        echo 'Trampoline for ', $name, PHP_EOL;
+    }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'trampoline'];
+
+echo "Send headers.\n";
+header_register_callback($callback);
+?>
+--EXPECT--
+Send headers.