Skip to content

Fix UAF issues with PCRE after request shutdown #15205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ PHP 8.4 INTERNALS UPGRADE NOTES
- pcre_get_compiled_regex_cache_ex() now provides an option to collect extra
options (from modifiers used in the expression, for example), and calls
pcre2_set_compile_extra_options() with those options.
- Removed per-request cache, the cache is now always per process or
per thread depending on whether you use NTS or ZTS.
This was removed due to fundamental ordering issues between destructors.

g. ext/standard
- Added the php_base64_encode_ex() API with flag parameters, value can be
Expand Down
4 changes: 0 additions & 4 deletions ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -2597,10 +2597,6 @@ static void accel_reset_pcre_cache(void)
{
Bucket *p;

if (PCRE_G(per_request_cache)) {
return;
}

ZEND_HASH_MAP_FOREACH_BUCKET(&PCRE_G(pcre_cache), p) {
/* Remove PCRE cache entries with inconsistent keys */
if (zend_accel_in_shm(p->key)) {
Expand Down
93 changes: 39 additions & 54 deletions ext/pcre/php_pcre.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ static MUTEX_T pcre_mt = NULL;

ZEND_TLS HashTable char_tables;

static void free_subpats_table(zend_string **subpat_names, uint32_t num_subpats, bool persistent);
static void free_subpats_table(zend_string **subpat_names, uint32_t num_subpats);

static void php_pcre_free_char_table(zval *data)
{/*{{{*/
Expand Down Expand Up @@ -168,25 +168,13 @@ static void php_free_pcre_cache(zval *data) /* {{{ */
pcre_cache_entry *pce = (pcre_cache_entry *) Z_PTR_P(data);
if (!pce) return;
if (pce->subpats_table) {
free_subpats_table(pce->subpats_table, pce->capture_count + 1, true);
free_subpats_table(pce->subpats_table, pce->capture_count + 1);
}
pcre2_code_free(pce->re);
free(pce);
}
/* }}} */

static void php_efree_pcre_cache(zval *data) /* {{{ */
{
pcre_cache_entry *pce = (pcre_cache_entry *) Z_PTR_P(data);
if (!pce) return;
if (pce->subpats_table) {
free_subpats_table(pce->subpats_table, pce->capture_count + 1, false);
}
pcre2_code_free(pce->re);
efree(pce);
}
/* }}} */

static void *php_pcre_malloc(PCRE2_SIZE size, void *data)
{
return pemalloc(size, 1);
Expand Down Expand Up @@ -303,12 +291,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */
{
php_pcre_mutex_alloc();

/* If we're on the CLI SAPI, there will only be one request, so we don't need the
* cache to survive after RSHUTDOWN. */
pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0;
if (!pcre_globals->per_request_cache) {
zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1);
}
zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1);

pcre_globals->backtrack_limit = 0;
pcre_globals->recursion_limit = 0;
Expand All @@ -326,9 +309,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */

static PHP_GSHUTDOWN_FUNCTION(pcre) /* {{{ */
{
if (!pcre_globals->per_request_cache) {
zend_hash_destroy(&pcre_globals->pcre_cache);
}
zend_hash_destroy(&pcre_globals->pcre_cache);

php_pcre_shutdown_pcre2();
zend_hash_destroy(&char_tables);
Expand Down Expand Up @@ -491,10 +472,6 @@ static PHP_RINIT_FUNCTION(pcre)
return FAILURE;
}

if (PCRE_G(per_request_cache)) {
zend_hash_init(&PCRE_G(pcre_cache), 0, NULL, php_efree_pcre_cache, 0);
}

return SUCCESS;
}
/* }}} */
Expand All @@ -504,10 +481,6 @@ static PHP_RSHUTDOWN_FUNCTION(pcre)
pcre2_general_context_free(PCRE_G(gctx_zmm));
PCRE_G(gctx_zmm) = NULL;

if (PCRE_G(per_request_cache)) {
zend_hash_destroy(&PCRE_G(pcre_cache));
}
Comment on lines -507 to -509
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the description and what I've understood at so far... couldn't you re-initialize this to an empty table after the zend_hash_destroy? No more use-after-free, no other code changes necessary because it's still got a valid hash table in it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change described as-is would only work for the resource test. The spl test would still cause a uaf because it references the pcre cache entry directly. That would be solvable by always looking up the instance from the cache though.
However, even with the uaf out of the way, the issue still wouldn't be fixed as the pcre entry no longer exists, so then we end up with an exception on cli while the code works fine on non-cli SAPIs. We could fix that by allowing to re-add entries to the hash table, but that is all messy and complicated and opens the question of who needs to clean up the cache entries after re-adding them after they were removed in the first place.


zval_ptr_dtor(&PCRE_G(unmatched_null_pair));
zval_ptr_dtor(&PCRE_G(unmatched_empty_pair));
ZVAL_UNDEF(&PCRE_G(unmatched_null_pair));
Expand All @@ -530,18 +503,18 @@ static int pcre_clean_cache(zval *data, void *arg)
}
/* }}} */

static void free_subpats_table(zend_string **subpat_names, uint32_t num_subpats, bool persistent) {
static void free_subpats_table(zend_string **subpat_names, uint32_t num_subpats) {
uint32_t i;
for (i = 0; i < num_subpats; i++) {
if (subpat_names[i]) {
zend_string_release_ex(subpat_names[i], persistent);
zend_string_release_ex(subpat_names[i], true);
}
}
pefree(subpat_names, persistent);
pefree(subpat_names, true);
}

/* {{{ static make_subpats_table */
static zend_string **make_subpats_table(uint32_t name_cnt, pcre_cache_entry *pce, bool persistent)
static zend_string **make_subpats_table(uint32_t name_cnt, pcre_cache_entry *pce)
{
uint32_t num_subpats = pce->capture_count + 1;
uint32_t name_size, ni = 0;
Expand All @@ -556,7 +529,7 @@ static zend_string **make_subpats_table(uint32_t name_cnt, pcre_cache_entry *pce
return NULL;
}

subpat_names = pecalloc(num_subpats, sizeof(zend_string *), persistent);
subpat_names = pecalloc(num_subpats, sizeof(zend_string *), true);
while (ni++ < name_cnt) {
unsigned short name_idx = 0x100 * (unsigned char)name_table[0] + (unsigned char)name_table[1];
const char *name = name_table + 2;
Expand All @@ -566,10 +539,8 @@ static zend_string **make_subpats_table(uint32_t name_cnt, pcre_cache_entry *pce
* Although we will be storing them in user-exposed arrays, they cannot cause problems
* because they only live in this thread and the last reference is deleted on shutdown
* instead of by user code. */
subpat_names[name_idx] = zend_string_init(name, strlen(name), persistent);
if (persistent) {
GC_MAKE_PERSISTENT_LOCAL(subpat_names[name_idx]);
}
subpat_names[name_idx] = zend_string_init(name, strlen(name), true);
GC_MAKE_PERSISTENT_LOCAL(subpat_names[name_idx]);
name_table += name_size;
}
return subpat_names;
Expand Down Expand Up @@ -871,7 +842,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo

/* Compute and cache the subpattern table to avoid computing it again over and over. */
if (name_count > 0) {
new_entry.subpats_table = make_subpats_table(name_count, &new_entry, !PCRE_G(per_request_cache));
new_entry.subpats_table = make_subpats_table(name_count, &new_entry);
if (!new_entry.subpats_table) {
if (key != regex) {
zend_string_release_ex(key, false);
Expand All @@ -892,7 +863,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo
* as hash keys especually for this table.
* See bug #63180
*/
if (!(GC_FLAGS(key) & IS_STR_PERMANENT) && !PCRE_G(per_request_cache)) {
if (!(GC_FLAGS(key) & IS_STR_PERMANENT)) {
zend_string *str = zend_string_init(ZSTR_VAL(key), ZSTR_LEN(key), 1);
GC_MAKE_PERSISTENT_LOCAL(str);

Expand Down Expand Up @@ -963,18 +934,18 @@ PHPAPI void php_pcre_free_match_data(pcre2_match_data *match_data)
}
}/*}}}*/

static void init_unmatched_null_pair(void) {
static void init_unmatched_null_pair(zval *pair) {
zval val1, val2;
ZVAL_NULL(&val1);
ZVAL_LONG(&val2, -1);
ZVAL_ARR(&PCRE_G(unmatched_null_pair), zend_new_pair(&val1, &val2));
ZVAL_ARR(pair, zend_new_pair(&val1, &val2));
}

static void init_unmatched_empty_pair(void) {
static void init_unmatched_empty_pair(zval *pair) {
zval val1, val2;
ZVAL_EMPTY_STRING(&val1);
ZVAL_LONG(&val2, -1);
ZVAL_ARR(&PCRE_G(unmatched_empty_pair), zend_new_pair(&val1, &val2));
ZVAL_ARR(pair, zend_new_pair(&val1, &val2));
}

static zend_always_inline void populate_match_value_str(
Expand Down Expand Up @@ -1020,15 +991,29 @@ static inline void add_offset_pair(
/* Add (match, offset) to the return value */
if (PCRE2_UNSET == start_offset) {
if (unmatched_as_null) {
if (Z_ISUNDEF(PCRE_G(unmatched_null_pair))) {
init_unmatched_null_pair();
}
ZVAL_COPY(&match_pair, &PCRE_G(unmatched_null_pair));
do {
if (Z_ISUNDEF(PCRE_G(unmatched_null_pair))) {
if (UNEXPECTED(EG(flags) & EG_FLAGS_IN_SHUTDOWN)) {
init_unmatched_null_pair(&match_pair);
break;
} else {
init_unmatched_null_pair(&PCRE_G(unmatched_null_pair));
}
}
ZVAL_COPY(&match_pair, &PCRE_G(unmatched_null_pair));
} while (0);
} else {
if (Z_ISUNDEF(PCRE_G(unmatched_empty_pair))) {
init_unmatched_empty_pair();
}
ZVAL_COPY(&match_pair, &PCRE_G(unmatched_empty_pair));
do {
if (Z_ISUNDEF(PCRE_G(unmatched_empty_pair))) {
if (UNEXPECTED(EG(flags) & EG_FLAGS_IN_SHUTDOWN)) {
init_unmatched_empty_pair(&match_pair);
break;
} else {
init_unmatched_empty_pair(&PCRE_G(unmatched_empty_pair));
}
}
ZVAL_COPY(&match_pair, &PCRE_G(unmatched_empty_pair));
} while (0);
}
} else {
zval val1, val2;
Expand Down
1 change: 0 additions & 1 deletion ext/pcre/php_pcre.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ ZEND_BEGIN_MODULE_GLOBALS(pcre)
#ifdef HAVE_PCRE_JIT_SUPPORT
bool jit;
#endif
bool per_request_cache;
php_pcre_error_code error_code;
/* Used for unmatched subpatterns in OFFSET_CAPTURE mode */
zval unmatched_null_pair;
Expand Down
12 changes: 12 additions & 0 deletions ext/pcre/tests/gh15205_1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
GH-15205: UAF when destroying RegexIterator after pcre request shutdown
--CREDITS--
YuanchengJiang
--FILE--
<?php
$array = new ArrayIterator(array('a', array('b', 'c')));
$regex = [new RegexIterator($array, '/Array/')];
echo "Done\n";
?>
--EXPECT--
Done
32 changes: 32 additions & 0 deletions ext/pcre/tests/gh15205_2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
GH-15205: UAF when destroying stream after pcre request shutdown
--CREDITS--
nicolaslegland
--FILE--
<?php
// Prime cache
preg_replace('/pattern/', 'replace', 'subject');

class wrapper
{
public function stream_open($path, $mode, $options, &$opened_path)
{
return true;
}

public function stream_close()
{
echo "Close\n";
preg_replace('/pattern/', 'replace', 'subject');
preg_match('/(4)?(2)?\d/', '23456', $matches, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL);
preg_match('/(4)?(2)?\d/', '23456', $matches, PREG_OFFSET_CAPTURE);
}

public $context;
}

stream_wrapper_register('wrapper', 'wrapper');
$handle = fopen('wrapper://', 'rb');
?>
--EXPECT--
Close
Loading