From 1709689256e2a92a7a515a3923c1e968da044a60 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:06:06 +0100 Subject: [PATCH 1/3] Fix GH-20906: Assertion failure when messing up output buffers Closes GH-20908. --- NEWS | 3 +++ ext/standard/basic_functions.c | 15 +++++++++++--- ext/standard/tests/strings/gh20906_1.phpt | 25 +++++++++++++++++++++++ ext/standard/tests/strings/gh20906_2.phpt | 21 +++++++++++++++++++ ext/standard/tests/strings/gh20906_3.phpt | 21 +++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/strings/gh20906_1.phpt create mode 100644 ext/standard/tests/strings/gh20906_2.phpt create mode 100644 ext/standard/tests/strings/gh20906_3.phpt diff --git a/NEWS b/NEWS index 1a4b588aa1e0..00f66935d9b3 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.20 +- Standard: + . Fixed bug GH-20906 (Assertion failure when messing up output buffers). + (ndossche) 12 Mar 2026, PHP 8.4.19 diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 16c34a21966e..cf54fbbe651c 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1768,7 +1768,10 @@ PHP_FUNCTION(highlight_file) } if (i) { - php_output_start_default(); + if (UNEXPECTED(php_output_start_default() != SUCCESS)) { + zend_throw_error(NULL, "Unable to start output handler"); + RETURN_THROWS(); + } } php_get_highlight_struct(&syntax_highlighter_ini); @@ -1803,7 +1806,10 @@ PHP_FUNCTION(php_strip_whitespace) Z_PARAM_PATH_STR(filename) ZEND_PARSE_PARAMETERS_END(); - php_output_start_default(); + if (UNEXPECTED(php_output_start_default() != SUCCESS)) { + zend_throw_error(NULL, "Unable to start output handler"); + RETURN_THROWS(); + } zend_stream_init_filename_ex(&file_handle, filename); zend_save_lexical_state(&original_lex_state); @@ -1840,7 +1846,10 @@ PHP_FUNCTION(highlight_string) ZEND_PARSE_PARAMETERS_END(); if (i) { - php_output_start_default(); + if (UNEXPECTED(php_output_start_default() != SUCCESS)) { + zend_throw_error(NULL, "Unable to start output handler"); + RETURN_THROWS(); + } } EG(error_reporting) = E_ERROR; diff --git a/ext/standard/tests/strings/gh20906_1.phpt b/ext/standard/tests/strings/gh20906_1.phpt new file mode 100644 index 000000000000..ccb0dfbee566 --- /dev/null +++ b/ext/standard/tests/strings/gh20906_1.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-20906 (Assertion failure when messing up output buffers) - php_strip_whitespace +--CREDITS-- +vi3tL0u1s +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECTF-- +%a +Fatal error: php_strip_whitespace(): Cannot use output buffering in output buffering display handlers in %s on line %d diff --git a/ext/standard/tests/strings/gh20906_2.phpt b/ext/standard/tests/strings/gh20906_2.phpt new file mode 100644 index 000000000000..b3ea5cf6ef74 --- /dev/null +++ b/ext/standard/tests/strings/gh20906_2.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-20906 (Assertion failure when messing up output buffers) - highlight_file +--CREDITS-- +vi3tL0u1s +--FILE-- + +--EXPECTF-- +%a +Fatal error: highlight_file(): Cannot use output buffering in output buffering display handlers in %s on line %d diff --git a/ext/standard/tests/strings/gh20906_3.phpt b/ext/standard/tests/strings/gh20906_3.phpt new file mode 100644 index 000000000000..a26fc61bed65 --- /dev/null +++ b/ext/standard/tests/strings/gh20906_3.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-20906 (Assertion failure when messing up output buffers) - highlight_string +--CREDITS-- +vi3tL0u1s +--FILE-- + +--EXPECTF-- +%a +Fatal error: highlight_string(): Cannot use output buffering in output buffering display handlers in %s on line %d From 27e12b50f4d430c8f98bc58f534e438bfa2ecd62 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 26 Feb 2026 20:24:24 +0000 Subject: [PATCH 2/3] ext/pcre: drop negative-length match tests Newer PCRE2 rejects \K in lookarounds at compile time, so the negative-length match code path can no longer be triggered from userland PHP. The PCRE2_EXTRA_ALLOW_LOOKAROUND_BSK flag that would re-enable it is only settable via the C compile context API. --- .../tests/preg_match_all_negative_length_match.phpt | 10 ---------- ext/pcre/tests/preg_match_negative_length_match.phpt | 10 ---------- 2 files changed, 20 deletions(-) delete mode 100644 ext/pcre/tests/preg_match_all_negative_length_match.phpt delete mode 100644 ext/pcre/tests/preg_match_negative_length_match.phpt diff --git a/ext/pcre/tests/preg_match_all_negative_length_match.phpt b/ext/pcre/tests/preg_match_all_negative_length_match.phpt deleted file mode 100644 index b18007cd3256..000000000000 --- a/ext/pcre/tests/preg_match_all_negative_length_match.phpt +++ /dev/null @@ -1,10 +0,0 @@ ---TEST-- -preg_match_all() resource cleanup when \K in lookahead causes negative-length match ---FILE-- - ---EXPECTF-- -Warning: preg_match_all(): Compilation failed: \K is not allowed in lookarounds (but see PCRE2_EXTRA_ALLOW_LOOKAROUND_BSK) at offset %d in %s -bool(false) diff --git a/ext/pcre/tests/preg_match_negative_length_match.phpt b/ext/pcre/tests/preg_match_negative_length_match.phpt deleted file mode 100644 index 221ea4fb9e54..000000000000 --- a/ext/pcre/tests/preg_match_negative_length_match.phpt +++ /dev/null @@ -1,10 +0,0 @@ ---TEST-- -preg_match() resource cleanup when \K in lookahead causes negative-length match ---FILE-- - ---EXPECTF-- -Warning: preg_match(): Compilation failed: \K is not allowed in lookarounds (but see PCRE2_EXTRA_ALLOW_LOOKAROUND_BSK) at offset %d in %s -bool(false) From f8114f554c7f3fb5495a288be5349a97eb43273a Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 25 Feb 2026 06:13:06 +0000 Subject: [PATCH 3/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 00f66935d9b3..cb621bd40014 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 ff53380afaeb..bfc0d6281bf6 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 000000000000..5fe4071e4fe1 --- /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"