From 5bd1bbdc50f9a56056cd8aec97f6ad04e5ffce4e Mon Sep 17 00:00:00 2001 From: Louis-Arnaud Date: Thu, 26 Feb 2026 17:14:53 +0100 Subject: [PATCH 1/5] ext/pcre: Fix preg_grep() returning partial array instead of false on error (#21260) When a PCRE execution error occurs (e.g. malformed UTF-8 with /u modifier), preg_grep() was returning a partial result array containing only the entries processed before the error. All other preg_* functions return false on execution errors. After the match loop, check PCRE_G(error_code) and if an error occurred, destroy the partial array and return false instead. Fixes GH-11936 --- UPGRADING | 5 +++ ext/pcre/php_pcre.c | 5 +++ ext/pcre/tests/preg_grep_error_utf8.phpt | 44 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 ext/pcre/tests/preg_grep_error_utf8.phpt diff --git a/UPGRADING b/UPGRADING index 77d8463afe990..e034d0b02359f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -19,6 +19,11 @@ PHP 8.6 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== +- PCRE: + . preg_grep() now returns false instead of a partial array when a PCRE + execution error occurs (e.g. malformed UTF-8 input with the /u modifier). + This is consistent with other preg_* functions. + - Phar: . Invalid values now throw in Phar::mungServer() instead of being silently ignored. diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 4efe0542b7638..72a923e650ba1 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -2987,6 +2987,11 @@ PHPAPI void php_pcre_grep_impl(pcre_cache_entry *pce, zval *input, zval *return if (match_data != mdata) { pcre2_match_data_free(match_data); } + + if (PCRE_G(error_code) != PHP_PCRE_NO_ERROR) { + zend_array_destroy(Z_ARR_P(return_value)); + RETURN_FALSE; + } } /* }}} */ diff --git a/ext/pcre/tests/preg_grep_error_utf8.phpt b/ext/pcre/tests/preg_grep_error_utf8.phpt new file mode 100644 index 0000000000000..efdd7632ab711 --- /dev/null +++ b/ext/pcre/tests/preg_grep_error_utf8.phpt @@ -0,0 +1,44 @@ +--TEST-- +preg_grep() returns false on match execution error (e.g. malformed UTF-8) +--FILE-- + +--EXPECTF-- +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +array(2) { + [0]=> + string(3) "foo" + [1]=> + string(3) "bar" +} +bool(true) From e045b881cf835287dd3fdbd06b72b01e960fed9a Mon Sep 17 00:00:00 2001 From: Peter Kokot Date: Thu, 26 Feb 2026 17:54:34 +0100 Subject: [PATCH 2/5] run-tests.php: Use PHP_SHLIB_SUFFIX (#21301) Instead of hardcoding the file extensions for shared PHP extensions (which in most cases are '.so', or '.dll'), this uses file extension for the current PHP build as it was defined during the configure/build phase. There can be systems where some other file extension is used for shared modules. --- run-tests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.php b/run-tests.php index 634beffa533ca..68d2cb69256da 100755 --- a/run-tests.php +++ b/run-tests.php @@ -859,7 +859,7 @@ function write_information(array $user_tests, $phpdbg): void $exts = get_loaded_extensions(); $ext_dir = ini_get('extension_dir'); foreach (scandir($ext_dir) as $file) { - if (preg_match('/^(?:php_)?([_a-zA-Z0-9]+)\.(?:so|dll)$/', $file, $matches)) { + if (preg_match('/^(?:php_)?([_a-zA-Z0-9]+)\.(?:' . PHP_SHLIB_SUFFIX . ')$/', $file, $matches)) { if (!extension_loaded($matches[1])) { $exts[] = $matches[1]; } From 9b01f517012b2612d0e1c8789eb540a36faf1346 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 23 Feb 2026 21:56:02 +0000 Subject: [PATCH 3/5] ext/pcntl: Reject negative values in pcntl_alarm() internal refactorings: - pcntl_signal_get_handler() max signals handling simplification, reusing the num_signals global. - pcntl_alarm() accepts a zend_long (signed) but passes it to alarm(), which takes an unsigned int. Negative values silently wrap to large unsigned values, scheduling an alarm far in the future instead of raising an error. Also reject large values above unsigned long max value. close GH-21282 --- UPGRADING | 4 +++ ext/pcntl/pcntl.c | 22 +++++------- .../tests/pcntl_alarm_invalid_value.phpt | 35 +++++++++++++++++++ 3 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 ext/pcntl/tests/pcntl_alarm_invalid_value.phpt diff --git a/UPGRADING b/UPGRADING index e034d0b02359f..9a1b37d98f7f4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -105,6 +105,10 @@ PHP 8.6 UPGRADE NOTES . Output of openssl_x509_parse() contains criticalExtensions listing all critical certificate extensions. +- PCNTL: + . pcntl_alarm() now throws a ValueError if the seconds argument is + lower than zero or greater than platform's UINT_MAX. + - Phar: . Phar::mungServer() now supports reference values. diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 53286723e1980..0ef87fffa287e 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -212,7 +212,7 @@ PHP_RINIT_FUNCTION(pcntl) PCNTL_G(last_error) = 0; PCNTL_G(num_signals) = NSIG; #ifdef SIGRTMAX - /* At least FreeBSD reports an incorrecrt NSIG that does not include realtime signals. + /* At least FreeBSD reports an incorrect NSIG that does not include realtime signals. * As SIGRTMAX may be a dynamic value, adjust the value in INIT. */ if (NSIG < SIGRTMAX + 1) { PCNTL_G(num_signals) = SIGRTMAX + 1; @@ -314,6 +314,11 @@ PHP_FUNCTION(pcntl_alarm) Z_PARAM_LONG(seconds); ZEND_PARSE_PARAMETERS_END(); + if (seconds < 0 || seconds > UINT_MAX) { + zend_argument_value_error(1, "must be between 0 and %u", UINT_MAX); + RETURN_THROWS(); + } + RETURN_LONG((zend_long) alarm(seconds)); } /* }}} */ @@ -870,17 +875,8 @@ PHP_FUNCTION(pcntl_signal_get_handler) Z_PARAM_LONG(signo) ZEND_PARSE_PARAMETERS_END(); - // note: max signal on mac is SIGUSR2 (31), no real time signals. - int sigmax = NSIG - 1; -#if defined(SIGRTMAX) - // oddily enough, NSIG on freebsd reports only 32 whereas SIGRTMIN starts at 65. - if (sigmax < SIGRTMAX) { - sigmax = SIGRTMAX; - } -#endif - - if (signo < 1 || signo > sigmax) { - zend_argument_value_error(1, "must be between 1 and %d", sigmax); + if (signo < 1 || signo >= PCNTL_G(num_signals)) { + zend_argument_value_error(1, "must be between 1 and %d", PCNTL_G(num_signals) - 1); RETURN_THROWS(); } @@ -1153,7 +1149,7 @@ static void pcntl_siginfo_to_zval(int signo, siginfo_t *siginfo, zval *user_sigi case SIGFPE: case SIGSEGV: case SIGBUS: - add_assoc_double_ex(user_siginfo, "addr", sizeof("addr")-1, (zend_long)siginfo->si_addr); + add_assoc_long_ex(user_siginfo, "addr", sizeof("addr")-1, (zend_long)siginfo->si_addr); break; #if defined(SIGPOLL) && !defined(__CYGWIN__) case SIGPOLL: diff --git a/ext/pcntl/tests/pcntl_alarm_invalid_value.phpt b/ext/pcntl/tests/pcntl_alarm_invalid_value.phpt new file mode 100644 index 0000000000000..59e74662f6f7c --- /dev/null +++ b/ext/pcntl/tests/pcntl_alarm_invalid_value.phpt @@ -0,0 +1,35 @@ +--TEST-- +pcntl_alarm() rejects invalid values +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +getMessage() . \PHP_EOL; +} + +try { + pcntl_alarm(PHP_INT_MIN); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} + +try { + pcntl_alarm(PHP_INT_MAX); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} + +var_dump(pcntl_alarm(0)); + +?> +--EXPECTF-- +pcntl_alarm(): Argument #1 ($seconds) must be between 0 and %d +pcntl_alarm(): Argument #1 ($seconds) must be between 0 and %d +pcntl_alarm(): Argument #1 ($seconds) must be between 0 and %d +int(0) From 78702fa470119e02df9323f8e18b8320ff55ded4 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 25 Feb 2026 20:34:00 +0000 Subject: [PATCH 4/5] ext/pcre: fix memory leaks on error paths Fix pcre2_code leak when pcre2_pattern_info() fails after a successful pcre2_compile(), and fix match_sets/match_data/marks leak when offsets[1] < offsets[0] in php_pcre_match_impl(). close GH-21298 --- NEWS | 3 +++ ext/pcre/php_pcre.c | 15 ++++++++++++++- .../preg_match_all_negative_length_match.phpt | 10 ++++++++++ .../tests/preg_match_negative_length_match.phpt | 10 ++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 ext/pcre/tests/preg_match_all_negative_length_match.phpt create mode 100644 ext/pcre/tests/preg_match_negative_length_match.phpt diff --git a/NEWS b/NEWS index 6fba1e4e64ae6..1a4b588aa1e02 100644 --- a/NEWS +++ b/NEWS @@ -60,6 +60,9 @@ PHP NEWS - PCRE: . Fixed preg_match memory leak with invalid regexes. (David Carlier) + . Fixed pcre2_code leak when pcre2_pattern_info() fails after a + successful pcre2_compile(), and match_sets/match_data/marks leaks + in php_pcre_match_impl(). (David Carlier) - PDO_PGSQL: . Fixed bug GH-21055 (connection attribute status typo for GSS negotiation). diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 24931466199c2..ff53380afaeb2 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -840,6 +840,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo if (key != regex) { zend_string_release_ex(key, 0); } + pcre2_code_free(new_entry.re); php_error_docref(NULL, E_WARNING, "Internal pcre2_pattern_info() error %d", rc); pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); return NULL; @@ -850,6 +851,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo if (key != regex) { zend_string_release_ex(key, 0); } + pcre2_code_free(new_entry.re); php_error_docref(NULL, E_WARNING, "Internal pcre_pattern_info() error %d", rc); pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); return NULL; @@ -1294,7 +1296,18 @@ PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str, if (subpats != NULL) { /* Try to get the list of substrings and display a warning if failed. */ if (UNEXPECTED(offsets[1] < offsets[0])) { - if (match_sets) efree(match_sets); + if (match_sets) { + for (i = 0; i < num_subpats; i++) { + zend_array_destroy(match_sets[i]); + } + efree(match_sets); + } + if (marks) { + zend_array_destroy(marks); + } + if (match_data != mdata) { + pcre2_match_data_free(match_data); + } php_error_docref(NULL, E_WARNING, "Get subpatterns list failed"); RETURN_FALSE; } diff --git a/ext/pcre/tests/preg_match_all_negative_length_match.phpt b/ext/pcre/tests/preg_match_all_negative_length_match.phpt new file mode 100644 index 0000000000000..0deb27749e196 --- /dev/null +++ b/ext/pcre/tests/preg_match_all_negative_length_match.phpt @@ -0,0 +1,10 @@ +--TEST-- +preg_match_all() resource cleanup when \K in lookahead causes negative-length match +--FILE-- + +--EXPECTF-- +Warning: preg_match_all(): Get subpatterns list failed in %s on line %d +bool(false) diff --git a/ext/pcre/tests/preg_match_negative_length_match.phpt b/ext/pcre/tests/preg_match_negative_length_match.phpt new file mode 100644 index 0000000000000..f321cb20b9c73 --- /dev/null +++ b/ext/pcre/tests/preg_match_negative_length_match.phpt @@ -0,0 +1,10 @@ +--TEST-- +preg_match() resource cleanup when \K in lookahead causes negative-length match +--FILE-- + +--EXPECTF-- +Warning: preg_match(): Get subpatterns list failed in %s on line %d +bool(false) From e64e8b0117172b1db98e0c1f9de4fe3ce88798a3 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 26 Feb 2026 19:09:43 +0000 Subject: [PATCH 5/5] ext/pcre: fix new pcre2 test close GH-21307 --- ext/pcre/tests/preg_match_all_negative_length_match.phpt | 2 +- ext/pcre/tests/preg_match_negative_length_match.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/pcre/tests/preg_match_all_negative_length_match.phpt b/ext/pcre/tests/preg_match_all_negative_length_match.phpt index 0deb27749e196..b18007cd32562 100644 --- a/ext/pcre/tests/preg_match_all_negative_length_match.phpt +++ b/ext/pcre/tests/preg_match_all_negative_length_match.phpt @@ -6,5 +6,5 @@ $result = preg_match_all('/(?=ab\K)a/', 'ab', $matches); var_dump($result); ?> --EXPECTF-- -Warning: preg_match_all(): Get subpatterns list failed in %s on line %d +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 index f321cb20b9c73..221ea4fb9e54c 100644 --- a/ext/pcre/tests/preg_match_negative_length_match.phpt +++ b/ext/pcre/tests/preg_match_negative_length_match.phpt @@ -6,5 +6,5 @@ $result = preg_match('/(?=ab\K)a/', 'ab', $matches); var_dump($result); ?> --EXPECTF-- -Warning: preg_match(): Get subpatterns list failed in %s on line %d +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)