Skip to content

Commit 38e2314

Browse files
committed
Fix GH-21639: Guard frameless reentry at handler sites
1 parent f7eb5ef commit 38e2314

File tree

7 files changed

+682
-51
lines changed

7 files changed

+682
-51
lines changed

Zend/tests/gh21639.phpt

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
--TEST--
2+
GH-21639: Frameless calls keep volatile arguments alive
3+
--FILE--
4+
<?php
5+
class ImplodeElement {
6+
public function __toString(): string {
7+
global $separator, $pieces;
8+
9+
$separator = null;
10+
$pieces = null;
11+
12+
return "C";
13+
}
14+
}
15+
16+
$separator = str_repeat(",", 1) . " ";
17+
$pieces = [new ImplodeElement(), 42];
18+
19+
var_dump(implode($separator, $pieces));
20+
var_dump($separator, $pieces);
21+
22+
class MutatingSeparator {
23+
public function __toString(): string {
24+
global $piecesFromSeparator;
25+
26+
$piecesFromSeparator = null;
27+
28+
return ", ";
29+
}
30+
}
31+
32+
$piecesFromSeparator = ["A", "B"];
33+
34+
var_dump(implode(new MutatingSeparator(), $piecesFromSeparator));
35+
var_dump($piecesFromSeparator);
36+
37+
class ImplodeElementWithoutSeparator {
38+
public function __toString(): string {
39+
global $oneArgPieces;
40+
41+
$oneArgPieces = null;
42+
43+
return "D";
44+
}
45+
}
46+
47+
$oneArgPieces = [new ImplodeElementWithoutSeparator(), 42];
48+
49+
var_dump(implode($oneArgPieces));
50+
var_dump($oneArgPieces);
51+
52+
class InArrayNeedle {
53+
public function __toString(): string {
54+
global $inArrayHaystack;
55+
56+
$inArrayHaystack = null;
57+
58+
return "needle";
59+
}
60+
}
61+
62+
$inArrayHaystack = [new InArrayNeedle()];
63+
64+
var_dump(in_array("needle", $inArrayHaystack));
65+
var_dump($inArrayHaystack);
66+
67+
class StrtrReplacement {
68+
public function __toString(): string {
69+
global $strtrReplacements;
70+
71+
$strtrReplacements = null;
72+
73+
return "b";
74+
}
75+
}
76+
77+
$strtrReplacements = ["a" => new StrtrReplacement()];
78+
79+
var_dump(strtr("a", $strtrReplacements));
80+
var_dump($strtrReplacements);
81+
82+
class StrReplaceSubject {
83+
public function __toString(): string {
84+
global $strReplaceSubject;
85+
86+
$strReplaceSubject = null;
87+
88+
return "a";
89+
}
90+
}
91+
92+
$strReplaceSubject = [new StrReplaceSubject(), "aa"];
93+
94+
var_dump(str_replace("a", "b", $strReplaceSubject));
95+
var_dump($strReplaceSubject);
96+
97+
class MinArg {
98+
public function __toString(): string {
99+
global $minArg;
100+
101+
$minArg = null;
102+
103+
return "a";
104+
}
105+
}
106+
107+
$minArg = new MinArg();
108+
$minResult = min($minArg, "b");
109+
110+
var_dump($minResult instanceof MinArg);
111+
var_dump($minArg);
112+
113+
class MaxArg {
114+
public function __toString(): string {
115+
global $maxArg;
116+
117+
$maxArg = null;
118+
119+
return "a";
120+
}
121+
}
122+
123+
$maxArg = new MaxArg();
124+
$maxResult = max("0", $maxArg);
125+
126+
var_dump($maxResult instanceof MaxArg);
127+
var_dump($maxArg);
128+
?>
129+
--EXPECT--
130+
string(5) "C, 42"
131+
NULL
132+
NULL
133+
string(4) "A, B"
134+
NULL
135+
string(3) "D42"
136+
NULL
137+
bool(true)
138+
NULL
139+
string(1) "b"
140+
NULL
141+
array(2) {
142+
[0]=>
143+
string(1) "b"
144+
[1]=>
145+
string(2) "bb"
146+
}
147+
NULL
148+
bool(true)
149+
NULL
150+
bool(true)
151+
NULL

Zend/tests/gh21639_parse_time.phpt

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
--TEST--
2+
GH-21639: Frameless parse-time conversions keep sibling arguments alive
3+
--FILE--
4+
<?php
5+
class TrimInput {
6+
public function __toString(): string {
7+
global $trimChars;
8+
9+
$trimChars = null;
10+
11+
return "---value---";
12+
}
13+
}
14+
15+
$trimChars = "-";
16+
var_dump(trim(new TrimInput(), $trimChars));
17+
var_dump($trimChars);
18+
19+
class ContainsNeedle {
20+
public function __toString(): string {
21+
global $containsHaystack;
22+
23+
$containsHaystack = null;
24+
25+
return "needle";
26+
}
27+
}
28+
29+
$containsHaystack = "needle in a haystack";
30+
var_dump(str_contains($containsHaystack, new ContainsNeedle()));
31+
var_dump($containsHaystack);
32+
33+
class RefContainsNeedle {
34+
public function __toString(): string {
35+
global $refContainsHaystack;
36+
37+
$refContainsHaystack = null;
38+
39+
return "hay";
40+
}
41+
}
42+
43+
$refContainsHaystack = "haystack";
44+
$refContainsNeedleObject = new RefContainsNeedle();
45+
$refContainsNeedle = &$refContainsNeedleObject;
46+
var_dump(str_contains($refContainsHaystack, $refContainsNeedle));
47+
var_dump($refContainsHaystack);
48+
49+
class SubstrInput {
50+
public function __toString(): string {
51+
global $substrOffset;
52+
53+
$substrOffset = null;
54+
55+
return "abcdef";
56+
}
57+
}
58+
59+
$substrOffset = 2;
60+
var_dump(substr(new SubstrInput(), $substrOffset));
61+
var_dump($substrOffset);
62+
63+
class DirnameInput {
64+
public function __toString(): string {
65+
global $dirnameLevels;
66+
67+
$dirnameLevels = null;
68+
69+
return "/var/www/project/app";
70+
}
71+
}
72+
73+
$dirnameLevels = 2;
74+
var_dump(dirname(new DirnameInput(), $dirnameLevels));
75+
var_dump($dirnameLevels);
76+
77+
class PregPattern {
78+
public function __toString(): string {
79+
global $pregSubject;
80+
81+
$pregSubject = null;
82+
83+
return "/foo/";
84+
}
85+
}
86+
87+
$pregSubject = "foobar";
88+
var_dump(preg_match(new PregPattern(), $pregSubject));
89+
var_dump($pregSubject);
90+
91+
class PregSubject {
92+
public function __toString(): string {
93+
global $pregPattern;
94+
95+
$pregPattern = null;
96+
97+
return "foobar";
98+
}
99+
}
100+
101+
$pregPattern = "/bar/";
102+
var_dump(preg_match($pregPattern, new PregSubject()));
103+
var_dump($pregPattern);
104+
?>
105+
--EXPECT--
106+
string(5) "value"
107+
NULL
108+
bool(true)
109+
NULL
110+
bool(true)
111+
NULL
112+
string(4) "cdef"
113+
NULL
114+
string(8) "/var/www"
115+
NULL
116+
int(1)
117+
NULL
118+
int(1)
119+
NULL

Zend/zend_frameless_function.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@
4242
#define ZEND_FRAMELESS_FUNCTION(name, arity) \
4343
void ZEND_FRAMELESS_FUNCTION_NAME(name, arity)(ZEND_FRAMELESS_FUNCTION_PARAMETERS_##arity)
4444

45+
#define Z_FLF_ARG_MAY_REENTER_STR_PARSE(arg) \
46+
(Z_TYPE_P(arg) == IS_OBJECT \
47+
|| (Z_TYPE_P(arg) == IS_REFERENCE && Z_TYPE_P(Z_REFVAL_P(arg)) == IS_OBJECT))
48+
49+
#define Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(parsed_arg_num, later_arg_num, tmp, protected) do { \
50+
if (UNEXPECTED(!protected && Z_FLF_ARG_MAY_REENTER_STR_PARSE(arg ## parsed_arg_num))) { \
51+
ZVAL_COPY(&tmp, arg ## later_arg_num); \
52+
arg ## later_arg_num = &tmp; \
53+
protected = true; \
54+
} \
55+
} while (0)
56+
57+
#define Z_FLF_FREE_PROTECTED_ARG(tmp, protected) do { \
58+
if (UNEXPECTED(protected)) { \
59+
zval_ptr_dtor(&tmp); \
60+
} \
61+
} while (0)
62+
63+
#define Z_FLF_PROTECT_ARRAY_ARG_FOR_REENTRY(arg_num, ht, tmp, protected) do { \
64+
if (UNEXPECTED(ht && !protected)) { \
65+
ZVAL_COPY(&tmp, arg ## arg_num); \
66+
arg ## arg_num = &tmp; \
67+
ht = Z_ARRVAL_P(arg ## arg_num); \
68+
protected = true; \
69+
} \
70+
} while (0)
71+
4572
#define Z_FLF_PARAM_ZVAL(arg_num, dest) \
4673
dest = arg ## arg_num;
4774
#define Z_FLF_PARAM_ARRAY(arg_num, dest) \

ext/pcre/php_pcre.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,8 +1499,29 @@ ZEND_FRAMELESS_FUNCTION(preg_match, 2)
14991499
{
15001500
zval regex_tmp, subject_tmp;
15011501
zend_string *regex, *subject;
1502+
zval arg1_copy, arg2_copy;
1503+
bool arg1_protected = false, arg2_protected = false;
1504+
1505+
if (EXPECTED(Z_TYPE_P(arg1) == IS_STRING && Z_TYPE_P(arg2) == IS_STRING)) {
1506+
/* Compile regex or get it from cache. */
1507+
pcre_cache_entry *pce;
1508+
if ((pce = pcre_get_compiled_regex_cache(Z_STR_P(arg1))) == NULL) {
1509+
RETVAL_FALSE;
1510+
return;
1511+
}
1512+
1513+
pce->refcount++;
1514+
php_pcre_match_impl(pce, Z_STR_P(arg2), return_value, /* subpats */ NULL,
1515+
/* global */ false, /* flags */ 0, /* start_offset */ 0);
1516+
pce->refcount--;
1517+
return;
1518+
}
15021519

1520+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(1, 2, arg2_copy, arg2_protected);
15031521
Z_FLF_PARAM_STR(1, regex, regex_tmp);
1522+
if (arg1 != &regex_tmp) {
1523+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(2, 1, arg1_copy, arg1_protected);
1524+
}
15041525
Z_FLF_PARAM_STR(2, subject, subject_tmp);
15051526

15061527
/* Compile regex or get it from cache. */
@@ -1518,6 +1539,8 @@ ZEND_FRAMELESS_FUNCTION(preg_match, 2)
15181539
flf_clean:
15191540
Z_FLF_PARAM_FREE_STR(1, regex_tmp);
15201541
Z_FLF_PARAM_FREE_STR(2, subject_tmp);
1542+
Z_FLF_FREE_PROTECTED_ARG(arg2_copy, arg2_protected);
1543+
Z_FLF_FREE_PROTECTED_ARG(arg1_copy, arg1_protected);
15211544
}
15221545

15231546
/* {{{ Perform a Perl-style global regular expression match */
@@ -2404,11 +2427,39 @@ ZEND_FRAMELESS_FUNCTION(preg_replace, 3)
24042427
zend_string *regex_str, *replace_str, *subject_str;
24052428
HashTable *regex_ht, *replace_ht, *subject_ht;
24062429
zval regex_tmp, replace_tmp, subject_tmp;
2430+
zval arg1_copy, arg2_copy, arg3_copy;
2431+
bool arg1_protected = false, arg2_protected = false, arg3_protected = false;
2432+
2433+
if (EXPECTED(Z_TYPE_P(arg1) == IS_STRING && Z_TYPE_P(arg2) == IS_STRING && Z_TYPE_P(arg3) == IS_STRING)) {
2434+
_preg_replace_common(
2435+
return_value,
2436+
/* regex_ht */ NULL, Z_STR_P(arg1),
2437+
/* replace_ht */ NULL, Z_STR_P(arg2),
2438+
/* subject_ht */ NULL, Z_STR_P(arg3),
2439+
/* limit */ -1, /* zcount */ NULL, /* is_filter */ false);
2440+
return;
2441+
}
24072442

2443+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(1, 2, arg2_copy, arg2_protected);
2444+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(1, 3, arg3_copy, arg3_protected);
24082445
Z_FLF_PARAM_ARRAY_HT_OR_STR(1, regex_ht, regex_str, regex_tmp);
2446+
if (arg1 != &regex_tmp) {
2447+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(2, 1, arg1_copy, arg1_protected);
2448+
}
2449+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(2, 3, arg3_copy, arg3_protected);
24092450
Z_FLF_PARAM_ARRAY_HT_OR_STR(2, replace_ht, replace_str, replace_tmp);
2451+
if (arg1 != &regex_tmp) {
2452+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(3, 1, arg1_copy, arg1_protected);
2453+
}
2454+
if (arg2 != &replace_tmp) {
2455+
Z_FLF_PROTECT_LATER_ARG_FOR_STR_PARSE(3, 2, arg2_copy, arg2_protected);
2456+
}
24102457
Z_FLF_PARAM_ARRAY_HT_OR_STR(3, subject_ht, subject_str, subject_tmp);
24112458

2459+
Z_FLF_PROTECT_ARRAY_ARG_FOR_REENTRY(1, regex_ht, arg1_copy, arg1_protected);
2460+
Z_FLF_PROTECT_ARRAY_ARG_FOR_REENTRY(2, replace_ht, arg2_copy, arg2_protected);
2461+
Z_FLF_PROTECT_ARRAY_ARG_FOR_REENTRY(3, subject_ht, arg3_copy, arg3_protected);
2462+
24122463
_preg_replace_common(
24132464
return_value,
24142465
regex_ht, regex_str,
@@ -2420,6 +2471,9 @@ flf_clean:;
24202471
Z_FLF_PARAM_FREE_STR(1, regex_tmp);
24212472
Z_FLF_PARAM_FREE_STR(2, replace_tmp);
24222473
Z_FLF_PARAM_FREE_STR(3, subject_tmp);
2474+
Z_FLF_FREE_PROTECTED_ARG(arg3_copy, arg3_protected);
2475+
Z_FLF_FREE_PROTECTED_ARG(arg2_copy, arg2_protected);
2476+
Z_FLF_FREE_PROTECTED_ARG(arg1_copy, arg1_protected);
24232477
}
24242478

24252479
/* {{{ Perform Perl-style regular expression replacement using replacement callback. */

0 commit comments

Comments
 (0)