Commit 78702fa4701 for php.net

commit 78702fa470119e02df9323f8e18b8320ff55ded4
Author: David Carlier <devnexen@gmail.com>
Date:   Wed Feb 25 20:34:00 2026 +0000

    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

diff --git a/NEWS b/NEWS
index 6fba1e4e64a..1a4b588aa1e 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 24931466199..ff53380afae 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 00000000000..0deb27749e1
--- /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--
+<?php
+$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
+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 00000000000..f321cb20b9c
--- /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--
+<?php
+$result = preg_match('/(?=ab\K)a/', 'ab', $matches);
+var_dump($result);
+?>
+--EXPECTF--
+Warning: preg_match(): Get subpatterns list failed in %s on line %d
+bool(false)