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"