Commit f8114f554c7 for php.net

commit f8114f554c7f3fb5495a288be5349a97eb43273a
Author: David Carlier <devnexen@gmail.com>
Date:   Wed Feb 25 06:13:06 2026 +0000

    ext/pcre: fix mdata_used race conditions in PCRE functions

    Mirror the mdata_used protection pattern from php_pcre_replace_func_impl
    in php_pcre_match_impl, php_pcre_replace_impl, php_pcre_split_impl,
    and php_pcre_grep_impl.

    close GH-21291

diff --git a/NEWS b/NEWS
index 00f66935d9b..cb621bd4001 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.4.20

+- PCRE:
+  . Fixed re-entrancy issue on php_pcre_match_impl, php_pcre_replace_impl,
+    php_pcre_split_impl, and php_pcre_grep_impl. (David Carlier)
+
 - Standard:
   . Fixed bug GH-20906 (Assertion failure when messing up output buffers).
     (ndossche)
diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c
index ff53380afae..bfc0d6281bf 100644
--- a/ext/pcre/php_pcre.c
+++ b/ext/pcre/php_pcre.c
@@ -1175,6 +1175,7 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	HashTable		*marks = NULL;		/* Array of marks for PREG_PATTERN_ORDER */
 	pcre2_match_data *match_data;
 	PCRE2_SIZE		 start_offset2, orig_start_offset;
+	bool old_mdata_used;

 	char *subject = ZSTR_VAL(subject_str);
 	size_t subject_len = ZSTR_LEN(subject_str);
@@ -1244,7 +1245,9 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	matched = 0;
 	PCRE_G(error_code) = PHP_PCRE_NO_ERROR;

-	if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+	old_mdata_used = mdata_used;
+	if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+		mdata_used = true;
 		match_data = mdata;
 	} else {
 		match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
@@ -1441,6 +1444,7 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	if (match_data != mdata) {
 		pcre2_match_data_free(match_data);
 	}
+	mdata_used = old_mdata_used;

 	/* Add the match sets to the output array and clean up */
 	if (match_sets) {
@@ -1645,6 +1649,7 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
 	size_t			result_len; 		/* Length of result */
 	zend_string		*result;			/* Result of replacement */
 	pcre2_match_data *match_data;
+	bool old_mdata_used;

 	/* Calculate the size of the offsets array, and allocate memory for it. */
 	num_subpats = pce->capture_count + 1;
@@ -1658,7 +1663,9 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
 	result_len = 0;
 	PCRE_G(error_code) = PHP_PCRE_NO_ERROR;

-	if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+	old_mdata_used = mdata_used;
+	if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+		mdata_used = true;
 		match_data = mdata;
 	} else {
 		match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
@@ -1860,6 +1867,7 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
 	if (match_data != mdata) {
 		pcre2_match_data_free(match_data);
 	}
+	mdata_used = old_mdata_used;

 	return result;
 }
@@ -2588,6 +2596,7 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	uint32_t		 num_subpats;		/* Number of captured subpatterns */
 	zval			 tmp;
 	pcre2_match_data *match_data;
+	bool old_mdata_used;
 	char *subject = ZSTR_VAL(subject_str);

 	no_empty = flags & PREG_SPLIT_NO_EMPTY;
@@ -2614,7 +2623,9 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
 		goto last;
 	}

-	if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+	old_mdata_used = mdata_used;
+	if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+		mdata_used = true;
 		match_data = mdata;
 	} else {
 		match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
@@ -2743,6 +2754,7 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
 	if (match_data != mdata) {
 		pcre2_match_data_free(match_data);
 	}
+	mdata_used = old_mdata_used;

 	if (PCRE_G(error_code) != PHP_PCRE_NO_ERROR) {
 		zval_ptr_dtor(return_value);
@@ -2942,6 +2954,7 @@ PHPAPI void  php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return
 	zend_ulong		 num_key;
 	bool		 invert;			/* Whether to return non-matching
 										   entries */
+	bool old_mdata_used;
 	pcre2_match_data *match_data;
 	invert = flags & PREG_GREP_INVERT ? 1 : 0;

@@ -2954,7 +2967,9 @@ PHPAPI void  php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return

 	PCRE_G(error_code) = PHP_PCRE_NO_ERROR;

-	if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+	old_mdata_used = mdata_used;
+	if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
+		mdata_used = true;
 		match_data = mdata;
 	} else {
 		match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
@@ -3019,6 +3034,7 @@ PHPAPI void  php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return
 	if (match_data != mdata) {
 		pcre2_match_data_free(match_data);
 	}
+	mdata_used = old_mdata_used;
 }
 /* }}} */

diff --git a/ext/pcre/tests/pcre_reentrancy.phpt b/ext/pcre/tests/pcre_reentrancy.phpt
new file mode 100644
index 00000000000..5fe4071e4fe
--- /dev/null
+++ b/ext/pcre/tests/pcre_reentrancy.phpt
@@ -0,0 +1,58 @@
+--TEST--
+PCRE re-entrancy: nested calls should not corrupt global match data
+--EXTENSIONS--
+pcre
+--FILE--
+<?php
+
+echo "Testing nested PCRE calls..." . PHP_EOL;
+
+$subject = 'abc';
+
+// preg_replace_callback is the most common way to trigger re-entrancy
+$result = preg_replace_callback('/./', function($m) {
+    $char = $m[0];
+    echo "Outer match: $char" . PHP_EOL;
+
+    // 1. Nested preg_match
+    preg_match('/./', 'inner', $inner_m);
+
+    // 2. Nested preg_replace (string version)
+    preg_replace('/n/', 'N', 'inner');
+
+    // 3. Nested preg_split
+    preg_split('/n/', 'inner');
+
+    // 4. Nested preg_grep
+    preg_grep('/n/', ['inner']);
+
+    // If any of the above stole the global mdata buffer without setting mdata_used,
+    // the 'offsets' used by this outer preg_replace_callback loop would be corrupted.
+
+    return strtoupper($char);
+}, $subject);
+
+var_dump($result);
+
+echo PHP_EOL . "Testing deep nesting..." . PHP_EOL;
+
+$result = preg_replace_callback('/a/', function($m) {
+    return preg_replace_callback('/b/', function($m) {
+        return preg_replace_callback('/c/', function($m) {
+            return "SUCCESS";
+        }, 'c');
+    }, 'b');
+}, 'a');
+
+var_dump($result);
+
+?>
+--EXPECT--
+Testing nested PCRE calls...
+Outer match: a
+Outer match: b
+Outer match: c
+string(3) "ABC"
+
+Testing deep nesting...
+string(7) "SUCCESS"