Skip to content

Commit 630dff8

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.
1 parent 90cb181 commit 630dff8

File tree

2 files changed

+82
-4
lines changed

2 files changed

+82
-4
lines changed

ext/pcre/php_pcre.c

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,7 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
11731173
HashTable *marks = NULL; /* Array of marks for PREG_PATTERN_ORDER */
11741174
pcre2_match_data *match_data;
11751175
PCRE2_SIZE start_offset2, orig_start_offset;
1176+
bool old_mdata_used;
11761177

11771178
char *subject = ZSTR_VAL(subject_str);
11781179
size_t subject_len = ZSTR_LEN(subject_str);
@@ -1242,12 +1243,15 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
12421243
matched = 0;
12431244
PCRE_G(error_code) = PHP_PCRE_NO_ERROR;
12441245

1245-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1246+
old_mdata_used = mdata_used;
1247+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1248+
mdata_used = 1;
12461249
match_data = mdata;
12471250
} else {
12481251
match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
12491252
if (!match_data) {
12501253
PCRE_G(error_code) = PHP_PCRE_INTERNAL_ERROR;
1254+
mdata_used = old_mdata_used;
12511255
RETURN_FALSE;
12521256
}
12531257
}
@@ -1428,6 +1432,7 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str,
14281432
if (match_data != mdata) {
14291433
pcre2_match_data_free(match_data);
14301434
}
1435+
mdata_used = old_mdata_used;
14311436

14321437
/* Add the match sets to the output array and clean up */
14331438
if (match_sets) {
@@ -1632,6 +1637,7 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
16321637
size_t result_len; /* Length of result */
16331638
zend_string *result; /* Result of replacement */
16341639
pcre2_match_data *match_data;
1640+
bool old_mdata_used;
16351641

16361642
/* Calculate the size of the offsets array, and allocate memory for it. */
16371643
num_subpats = pce->capture_count + 1;
@@ -1645,12 +1651,15 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
16451651
result_len = 0;
16461652
PCRE_G(error_code) = PHP_PCRE_NO_ERROR;
16471653

1648-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1654+
old_mdata_used = mdata_used;
1655+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
1656+
mdata_used = 1;
16491657
match_data = mdata;
16501658
} else {
16511659
match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
16521660
if (!match_data) {
16531661
PCRE_G(error_code) = PHP_PCRE_INTERNAL_ERROR;
1662+
mdata_used = old_mdata_used;
16541663
return NULL;
16551664
}
16561665
}
@@ -1847,6 +1856,7 @@ PHPAPI zend_string *php_pcre_replace_impl(pcre_cache_entry *pce, zend_string *su
18471856
if (match_data != mdata) {
18481857
pcre2_match_data_free(match_data);
18491858
}
1859+
mdata_used = old_mdata_used;
18501860

18511861
return result;
18521862
}
@@ -2575,6 +2585,7 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
25752585
uint32_t num_subpats; /* Number of captured subpatterns */
25762586
zval tmp;
25772587
pcre2_match_data *match_data;
2588+
bool old_mdata_used;
25782589
char *subject = ZSTR_VAL(subject_str);
25792590

25802591
no_empty = flags & PREG_SPLIT_NO_EMPTY;
@@ -2601,12 +2612,15 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
26012612
goto last;
26022613
}
26032614

2604-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2615+
old_mdata_used = mdata_used;
2616+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2617+
mdata_used = 1;
26052618
match_data = mdata;
26062619
} else {
26072620
match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
26082621
if (!match_data) {
26092622
PCRE_G(error_code) = PHP_PCRE_INTERNAL_ERROR;
2623+
mdata_used = old_mdata_used;
26102624
zval_ptr_dtor(return_value);
26112625
RETURN_FALSE;
26122626
}
@@ -2730,6 +2744,7 @@ PHPAPI void php_pcre_split_impl(pcre_cache_entry *pce, zend_string *subject_str,
27302744
if (match_data != mdata) {
27312745
pcre2_match_data_free(match_data);
27322746
}
2747+
mdata_used = old_mdata_used;
27332748

27342749
if (PCRE_G(error_code) != PHP_PCRE_NO_ERROR) {
27352750
zval_ptr_dtor(return_value);
@@ -2929,6 +2944,7 @@ PHPAPI void php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return
29292944
zend_ulong num_key;
29302945
bool invert; /* Whether to return non-matching
29312946
entries */
2947+
bool old_mdata_used;
29322948
pcre2_match_data *match_data;
29332949
invert = flags & PREG_GREP_INVERT ? 1 : 0;
29342950

@@ -2941,12 +2957,15 @@ PHPAPI void php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return
29412957

29422958
PCRE_G(error_code) = PHP_PCRE_NO_ERROR;
29432959

2944-
if (!mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2960+
old_mdata_used = mdata_used;
2961+
if (!old_mdata_used && num_subpats <= PHP_PCRE_PREALLOC_MDATA_SIZE) {
2962+
mdata_used = 1;
29452963
match_data = mdata;
29462964
} else {
29472965
match_data = pcre2_match_data_create_from_pattern(pce->re, PCRE_G(gctx_zmm));
29482966
if (!match_data) {
29492967
PCRE_G(error_code) = PHP_PCRE_INTERNAL_ERROR;
2968+
mdata_used = old_mdata_used;
29502969
return;
29512970
}
29522971
}
@@ -3006,6 +3025,7 @@ PHPAPI void php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return
30063025
if (match_data != mdata) {
30073026
pcre2_match_data_free(match_data);
30083027
}
3028+
mdata_used = old_mdata_used;
30093029
}
30103030
/* }}} */
30113031

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)