Commit cd383dcc377 for php

commit cd383dcc3770d4307755d4ac2f5887e4d8faa830
Author: Gina Peter Banyard <girgias@php.net>
Date:   Mon Oct 12 13:46:38 2026 +0100

    pcre: only use FCC for preg_replace_callback functions (#24152)

    Rather than resolving trampolines at every call we can simplify increase the refcount for the FCC.

diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c
index fce1a21a6c2..256b67e9e4c 100644
--- a/ext/pcre/php_pcre.c
+++ b/ext/pcre/php_pcre.c
@@ -1553,7 +1553,7 @@ static bool preg_get_backref(char **str, int *backref)
 }

 /* Return NULL if an exception has occurred */
-static zend_string *preg_do_repl_func(zend_fcall_info *fci, zend_fcall_info_cache *fcc, const zend_string *subject, PCRE2_SIZE *offsets, zend_string **subpat_names, uint32_t num_subpats, int count, const PCRE2_SPTR mark, zend_long flags)
+static zend_string *preg_do_repl_func(zend_fcall_info_cache *fcc, const zend_string *subject, PCRE2_SIZE *offsets, zend_string **subpat_names, uint32_t num_subpats, int count, const PCRE2_SPTR mark, zend_long flags)
 {
 	zend_string *result_str = NULL;
 	zval		 retval;			/* Function return value */
@@ -1562,11 +1562,7 @@ static zend_string *preg_do_repl_func(zend_fcall_info *fci, zend_fcall_info_cach
 	array_init_size(&arg, count + (mark ? 1 : 0));
 	populate_subpat_array(Z_ARRVAL(arg), ZSTR_VAL(subject), offsets, subpat_names, num_subpats, count, mark, flags);

-	fci->retval = &retval;
-	fci->param_count = 1;
-	fci->params = &arg;
-	fci->consumed_args = zend_fci_consumed_arg(0);
-	zend_call_function(fci, fcc);
+	zend_call_known_fcc_ex(fcc, &retval, 1, &arg, NULL, zend_fci_consumed_arg(0));
 	zval_ptr_dtor(&arg);
 	if (EXPECTED(Z_TYPE(retval) == IS_STRING)) {
 		return Z_STR(retval);
@@ -1854,7 +1850,7 @@ PHPAPI zend_string *php_pcre_replace_impl(const pcre_cache_entry *pce, zend_stri
 /* }}} */

 static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_string *subject_str,
-	zend_fcall_info *fci, zend_fcall_info_cache *fcc,
+	zend_fcall_info_cache *fcc,
 	size_t limit, size_t *replace_count, zend_long flags
 ) {
 	uint32_t		 options;			/* Execution options */
@@ -1952,7 +1948,7 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin

 			/* Use custom function to get replacement string and its length. */
 			zend_string *eval_result = preg_do_repl_func(
-				fci, fcc, subject_str, offsets, subpat_names, num_subpats, count,
+				fcc, subject_str, offsets, subpat_names, num_subpats, count,
 				pcre2_get_mark(match_data), flags);

 			if (UNEXPECTED(eval_result == NULL)) {
@@ -2055,7 +2051,7 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin

 static zend_always_inline zend_string *php_pcre_replace_func(zend_string *regex,
 							  zend_string *subject_str,
-							  zend_fcall_info *fci, zend_fcall_info_cache *fcc,
+							  zend_fcall_info_cache *fcc,
 							  size_t limit, size_t *replace_count, zend_long flags)
 {
 	pcre_cache_entry	*pce;			    /* Compiled regular expression */
@@ -2066,7 +2062,7 @@ static zend_always_inline zend_string *php_pcre_replace_func(zend_string *regex,
 		return NULL;
 	}
 	pce->refcount++;
-	result = php_pcre_replace_func_impl(pce, subject_str, fci, fcc, limit, replace_count, flags);
+	result = php_pcre_replace_func_impl(pce, subject_str, fcc, limit, replace_count, flags);
 	pce->refcount--;

 	return result;
@@ -2169,13 +2165,13 @@ static zend_always_inline zend_string *php_replace_in_subject(
 /* }}} */

 static zend_string *php_replace_in_subject_func(zend_string *regex_str, const HashTable *regex_ht,
-	zend_fcall_info *fci, zend_fcall_info_cache *fcc,
+	zend_fcall_info_cache *fcc,
 	zend_string *subject, size_t limit, size_t *replace_count, zend_long flags)
 {
 	zend_string *result;

 	if (regex_str) {
-		result = php_pcre_replace_func(regex_str, subject, fci, fcc, limit, replace_count, flags);
+		result = php_pcre_replace_func(regex_str, subject, fcc, limit, replace_count, flags);
 		return result;
 	} else {
 		/* If regex is an array */
@@ -2196,7 +2192,7 @@ static zend_string *php_replace_in_subject_func(zend_string *regex_str, const Ha
 			/* Do the actual replacement and put the result back into subject
 			   for further replacements. */
 			result = php_pcre_replace_func(
-				regex_entry_str, subject, fci, fcc, limit, replace_count, flags);
+				regex_entry_str, subject, fcc, limit, replace_count, flags);
 			zend_tmp_string_release(tmp_regex_entry_str);
 			zend_string_release(subject);
 			subject = result;
@@ -2211,7 +2207,7 @@ static zend_string *php_replace_in_subject_func(zend_string *regex_str, const Ha

 static size_t php_preg_replace_func_impl(zval *return_value,
 	zend_string *regex_str, const HashTable *regex_ht,
-	zend_fcall_info *fci, zend_fcall_info_cache *fcc,
+	zend_fcall_info_cache *fcc,
 	zend_string *subject_str, const HashTable *subject_ht, zend_long limit_val, zend_long flags)
 {
 	zend_string	*result;
@@ -2219,7 +2215,7 @@ static size_t php_preg_replace_func_impl(zval *return_value,

 	if (subject_str) {
 		result = php_replace_in_subject_func(
-			regex_str, regex_ht, fci, fcc, subject_str, limit_val, &replace_count, flags);
+			regex_str, regex_ht, fcc, subject_str, limit_val, &replace_count, flags);
 		if (result != NULL) {
 			RETVAL_STR(result);
 		} else {
@@ -2242,7 +2238,7 @@ static size_t php_preg_replace_func_impl(zval *return_value,
 			}

 			result = php_replace_in_subject_func(
-				regex_str, regex_ht, fci, fcc, subject_entry_str, limit_val, &replace_count, flags);
+				regex_str, regex_ht, fcc, subject_entry_str, limit_val, &replace_count, flags);
 			if (result != NULL) {
 				/* Add to return array */
 				zval zv;
@@ -2405,20 +2401,23 @@ PHP_FUNCTION(preg_replace_callback)
 	/* Get function parameters and do error-checking. */
 	ZEND_PARSE_PARAMETERS_START(3, 6)
 		Z_PARAM_ARRAY_HT_OR_STR(regex_ht, regex_str)
-		Z_PARAM_FUNC(fci, fcc)
+		Z_PARAM_FUNC_NO_TRAMPOLINE_FREE(fci, fcc)
 		Z_PARAM_ARRAY_HT_OR_STR(subject_ht, subject_str)
 		Z_PARAM_OPTIONAL
 		Z_PARAM_LONG(limit)
 		Z_PARAM_ZVAL(zcount)
 		Z_PARAM_LONG(flags)
-	ZEND_PARSE_PARAMETERS_END();
+	ZEND_PARSE_PARAMETERS_END_EX(goto free_trampoline);

 	replace_count = php_preg_replace_func_impl(return_value, regex_str, regex_ht,
-		&fci, &fcc,
+		&fcc,
 		subject_str, subject_ht, limit, flags);
 	if (zcount) {
 		ZEND_TRY_ASSIGN_REF_LONG(zcount, replace_count);
 	}
+
+free_trampoline:
+	zend_release_fcall_info_cache(&fcc);
 }
 /* }}} */

@@ -2454,10 +2453,6 @@ PHP_FUNCTION(preg_replace_callback_array)
 		}

 		zend_fcall_info_cache fcc = empty_fcall_info_cache;
-		zend_fcall_info fci = empty_fcall_info;
-		fci.size = sizeof(zend_fcall_info);
-		/* Copy potential trampoline */
-		ZVAL_COPY_VALUE(&fci.function_name, replace);

 		if (!zend_is_callable(replace, &fcc, NULL)) {
 			zend_argument_type_error(1, "must contain only valid callbacks");
@@ -2465,9 +2460,10 @@ PHP_FUNCTION(preg_replace_callback_array)
 		}

 		zval retval;
-		replace_count += php_preg_replace_func_impl(&retval, str_idx_regex, /* regex_ht */ NULL, &fci, &fcc,
+		zend_fcc_addref(&fcc);
+		replace_count += php_preg_replace_func_impl(&retval, str_idx_regex, /* regex_ht */ NULL, &fcc,
 			subject_str, subject_ht, limit, flags);
-		zend_release_fcall_info_cache(&fcc);
+		zend_fcc_dtor(&fcc);

 		switch (Z_TYPE(retval)) {
 			case IS_ARRAY:
diff --git a/ext/pcre/tests/preg_replace_callback_trampoline_zpp_freed.phpt b/ext/pcre/tests/preg_replace_callback_trampoline_zpp_freed.phpt
new file mode 100644
index 00000000000..ba249f46b24
--- /dev/null
+++ b/ext/pcre/tests/preg_replace_callback_trampoline_zpp_freed.phpt
@@ -0,0 +1,34 @@
+--TEST--
+preg_replace_callback() with a trampoline and a ZPP failure
+--FILE--
+<?php
+
+class TrampolineTest {
+    public function __call(string $name, array $arguments): string {
+        echo 'Trampoline for ', $name, PHP_EOL;
+        if ($name === 'trampolineThrow') {
+            throw new Exception('boo');
+        }
+        return "'" . $arguments[0][0] . "'";
+    }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'trampoline'];
+$callbackThrow = [$o, 'trampolineThrow'];
+
+$pattern = '@\b\w{1,2}\b@';
+
+$subject = 'a b3 bcd';
+
+$badLimit = 'not an int';
+
+try {
+    $matches = preg_replace_callback($pattern, $callbackThrow, $subject, $badLimit);
+    var_dump($matches);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+TypeError: preg_replace_callback(): Argument #4 ($limit) must be of type int, string given