Skip to content

Commit f8114f5

Browse files
committed
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
1 parent 1709689 commit f8114f5

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.20
44

5+
- PCRE:
6+
. Fixed re-entrancy issue on php_pcre_match_impl, php_pcre_replace_impl,
7+
php_pcre_split_impl, and php_pcre_grep_impl. (David Carlier)
8+
59
- Standard:
610
. Fixed bug GH-20906 (Assertion failure when messing up output buffers).
711
(ndossche)

ext/pcre/php_pcre.c

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,7 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
11751175
HashTable *marks = NULL; /* Array of marks for PREG_PATTERN_ORDER */
11761176
pcre2_match_data *match_data;
11771177
PCRE2_SIZE start_offset2, orig_start_offset;
1178+
bool old_mdata_used;
11781179

11791180
char *subject = ZSTR_VAL(subject_str);
11801181
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,
12441245
matched = 0;
12451246
PCRE_G(error_code) = PHP_PCRE_NO_ERROR;
12461247

1247-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1248+
old_mdata_used = mdata_used;
1249+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1250+
mdata_used = true;
12481251
match_data = mdata;
12491252
} else {
12501253
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,
14411444
if (match_data != mdata) {
14421445
pcre2_match_data_free(match_data);
14431446
}
1447+
mdata_used = old_mdata_used;
14441448

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

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

1661-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1666+
old_mdata_used = mdata_used;
1667+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1668+
mdata_used = true;
16621669
match_data = mdata;
16631670
} else {
16641671
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
18601867
if (match_data != mdata) {
18611868
pcre2_match_data_free(match_data);
18621869
}
1870+
mdata_used = old_mdata_used;
18631871

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

25932602
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,
26142623
goto last;
26152624
}
26162625

2617-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2626+
old_mdata_used = mdata_used;
2627+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2628+
mdata_used = true;
26182629
match_data = mdata;
26192630
} else {
26202631
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,
27432754
if (match_data != mdata) {
27442755
pcre2_match_data_free(match_data);
27452756
}
2757+
mdata_used = old_mdata_used;
27462758

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

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

29552968
PCRE_G(error_code) = PHP_PCRE_NO_ERROR;
29562969

2957-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2970+
old_mdata_used = mdata_used;
2971+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2972+
mdata_used = true;
29582973
match_data = mdata;
29592974
} else {
29602975
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
30193034
if (match_data != mdata) {
30203035
pcre2_match_data_free(match_data);
30213036
}
3037+
mdata_used = old_mdata_used;
30223038
}
30233039
/* }}} */
30243040

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
PCRE re-entrancy: nested calls should not corrupt global match data
3+
--EXTENSIONS--
4+
pcre
5+
--FILE--
6+
<?php
7+
8+
echo "Testing nested PCRE calls..." . PHP_EOL;
9+
10+
$subject = 'abc';
11+
12+
// preg_replace_callback is the most common way to trigger re-entrancy
13+
$result = preg_replace_callback('/./', function($m) {
14+
$char = $m[0];
15+
echo "Outer match: $char" . PHP_EOL;
16+
17+
// 1. Nested preg_match
18+
preg_match('/./', 'inner', $inner_m);
19+
20+
// 2. Nested preg_replace (string version)
21+
preg_replace('/n/', 'N', 'inner');
22+
23+
// 3. Nested preg_split
24+
preg_split('/n/', 'inner');
25+
26+
// 4. Nested preg_grep
27+
preg_grep('/n/', ['inner']);
28+
29+
// If any of the above stole the global mdata buffer without setting mdata_used,
30+
// the 'offsets' used by this outer preg_replace_callback loop would be corrupted.
31+
32+
return strtoupper($char);
33+
}, $subject);
34+
35+
var_dump($result);
36+
37+
echo PHP_EOL . "Testing deep nesting..." . PHP_EOL;
38+
39+
$result = preg_replace_callback('/a/', function($m) {
40+
return preg_replace_callback('/b/', function($m) {
41+
return preg_replace_callback('/c/', function($m) {
42+
return "SUCCESS";
43+
}, 'c');
44+
}, 'b');
45+
}, 'a');
46+
47+
var_dump($result);
48+
49+
?>
50+
--EXPECT--
51+
Testing nested PCRE calls...
52+
Outer match: a
53+
Outer match: b
54+
Outer match: c
55+
string(3) "ABC"
56+
57+
Testing deep nesting...
58+
string(7) "SUCCESS"

0 commit comments

Comments
 (0)