From f8114f554c7f3fb5495a288be5349a97eb43273a Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 25 Feb 2026 06:13:06 +0000 Subject: [PATCH 1/3] 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 --- NEWS | 4 ++ ext/pcre/php_pcre.c | 24 ++++++++++-- ext/pcre/tests/pcre_reentrancy.phpt | 58 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 ext/pcre/tests/pcre_reentrancy.phpt diff --git a/NEWS b/NEWS index 00f66935d9b3e..cb621bd40014f 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 ff53380afaeb2..bfc0d6281bf68 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 0000000000000..5fe4071e4fe17 --- /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-- + +--EXPECT-- +Testing nested PCRE calls... +Outer match: a +Outer match: b +Outer match: c +string(3) "ABC" + +Testing deep nesting... +string(7) "SUCCESS" From 4ee95fc2f30364a39afa6385abee2971e2d9a5c5 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:06:35 +0100 Subject: [PATCH 2/3] bz2: Fix truncation of total output size causing erroneous errors Also switch to uint64_t as that's used on master and makes the code simpler to fix. Closes GH-20807. Co-authored-by: Arnaud Le Blanc --- NEWS | 3 +++ ext/bz2/bz2.c | 17 ++++------------- ext/bz2/tests/gh20807.phpt | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 ext/bz2/tests/gh20807.phpt diff --git a/NEWS b/NEWS index cb621bd40014f..a990068937e70 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.20 +- Bz2: + . Fix truncation of total output size causing erroneous errors. (ndossche) + - 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) diff --git a/ext/bz2/bz2.c b/ext/bz2/bz2.c index 583d7f475a555..e43915e1992fd 100644 --- a/ext/bz2/bz2.c +++ b/ext/bz2/bz2.c @@ -502,11 +502,7 @@ PHP_FUNCTION(bzdecompress) size_t source_len; int error; bool small = 0; -#ifdef PHP_WIN32 - unsigned __int64 size = 0; -#else - unsigned long long size = 0; -#endif + uint64_t size = 0; bz_stream bzs; if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s|b", &source, &source_len, &small)) { @@ -531,27 +527,22 @@ PHP_FUNCTION(bzdecompress) while ((error = BZ2_bzDecompress(&bzs)) == BZ_OK && bzs.avail_in > 0) { /* compression is better then 2:1, need to allocate more memory */ bzs.avail_out = source_len; - size = (bzs.total_out_hi32 * (unsigned int) -1) + bzs.total_out_lo32; -#ifndef ZEND_ENABLE_ZVAL_LONG64 + size = (((uint64_t) bzs.total_out_hi32) << 32U) + bzs.total_out_lo32; if (size > SIZE_MAX) { /* no reason to continue if we're going to drop it anyway */ break; } -#endif dest = zend_string_safe_realloc(dest, 1, bzs.avail_out+1, (size_t) size, 0); bzs.next_out = ZSTR_VAL(dest) + size; } if (error == BZ_STREAM_END || error == BZ_OK) { - size = (bzs.total_out_hi32 * (unsigned int) -1) + bzs.total_out_lo32; -#ifndef ZEND_ENABLE_ZVAL_LONG64 + size = (((uint64_t) bzs.total_out_hi32) << 32U) + bzs.total_out_lo32; if (UNEXPECTED(size > SIZE_MAX)) { php_error_docref(NULL, E_WARNING, "Decompressed size too big, max is %zd", SIZE_MAX); zend_string_efree(dest); RETVAL_LONG(BZ_MEM_ERROR); - } else -#endif - { + } else { dest = zend_string_safe_realloc(dest, 1, (size_t)size, 1, 0); ZSTR_LEN(dest) = (size_t)size; ZSTR_VAL(dest)[(size_t)size] = '\0'; diff --git a/ext/bz2/tests/gh20807.phpt b/ext/bz2/tests/gh20807.phpt new file mode 100644 index 0000000000000..8d1e0cb40e4fd --- /dev/null +++ b/ext/bz2/tests/gh20807.phpt @@ -0,0 +1,19 @@ +--TEST-- +Truncation of total output size causing erroneous size and data +--EXTENSIONS-- +bz2 +--INI-- +memory_limit=-1 +--SKIPIF-- + +--FILE-- + +--EXPECT-- +string(40) "d4b5e52ed34a774fa645f94369b0c61375436d30" From b2bc70c4f748de63832e80029f4db16d2e2d0c74 Mon Sep 17 00:00:00 2001 From: Alex Dowad Date: Sat, 28 Feb 2026 09:53:43 +0900 Subject: [PATCH 3/3] Update time periods in mbstring section of EXTENSIONS --- EXTENSIONS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EXTENSIONS b/EXTENSIONS index 2eec7726c12c4..8da09aed5392a 100644 --- a/EXTENSIONS +++ b/EXTENSIONS @@ -372,8 +372,8 @@ STATUS: Working EXTENSION: mbstring PRIMARY MAINTAINER: Rui Hirokawa (2001 - 2013) Nikita Popov (2017 - 2020) - Alex Dowad (2021 - 2024) - Yuya Hamada (2024 - 2024) + Alex Dowad (2021 - 2026) + Yuya Hamada (2024 - 2026) MAINTENANCE: Maintained STATUS: Working -------------------------------------------------------------------------------