new file mode 100644
@@ -0,0 +1,221 @@
+From 1dcd0cf42a6a7cb62cc9a7c024196733abcfda95 Mon Sep 17 00:00:00 2001
+From: Nicholas Wilson <nicholas@nicholaswilson.me.uk>
+Date: Sat, 8 Aug 2026 19:17:36 +0100
+Subject: [PATCH] Fix leak & stale PCRE2_MD_COPIED_SUBJECT if pcre2_jit_match
+ used with existing match context (#937)
+
+The problem is not that pcre2_jit_match() needs to add support for PCRE2_COPY_MATCHED_SUBJECT. Instead, if the passed-in context somehow contains a previously-copied subject (by non-JIT matcher using a global or cached subject) then it will be leaked, and worse, incorrectly free'd later.
+
+CVE: CVE-2026-89161
+Upstream-Status: Backport [https://github.com/PCRE2Project/pcre2/commit/1dcd0cf42a6a7cb62cc9a7c024196733abcfda95]
+Signed-off-by: Peter Marko <peter.marko@siemens.com>
+---
+ doc/html/pcre2jit.html | 9 +++---
+ doc/pcre2.txt | 10 +++----
+ doc/pcre2jit.3 | 9 +++---
+ src/pcre2_jit_match_inc.h | 10 +++++++
+ src/pcre2test_inc.h | 60 ++++++++++++++++++++++++++++++++-------
+ testdata/testinput17 | 1 +
+ testdata/testoutput17 | 2 ++
+ 7 files changed, 77 insertions(+), 24 deletions(-)
+
+diff --git a/doc/html/pcre2jit.html b/doc/html/pcre2jit.html
+index cc26cc06..4e6d31e5 100644
+--- a/doc/html/pcre2jit.html
++++ b/doc/html/pcre2jit.html
+@@ -460,10 +460,11 @@ processed by <b>pcre2_jit_compile()</b>).
+ The fast path function is called <b>pcre2_jit_match()</b>, and it takes exactly
+ the same arguments as <b>pcre2_match()</b>. However, the subject string must be
+ specified with a length; PCRE2_ZERO_TERMINATED is not supported. Unsupported
+-option bits (for example, PCRE2_ANCHORED and PCRE2_ENDANCHORED) are ignored, as
+-is the PCRE2_NO_JIT option. The return values are also the same as for
+-<b>pcre2_match()</b>, plus PCRE2_ERROR_JIT_BADOPTION if a matching mode (partial
+-or complete) is requested that was not compiled.
++option bits (for example, PCRE2_ANCHORED, PCRE2_ENDANCHORED, and
++PCRE2_COPY_MATCHED_SUBJECT) are ignored, as is the PCRE2_NO_JIT option. The
++return values are also the same as for <b>pcre2_match()</b>, plus
++PCRE2_ERROR_JIT_BADOPTION if a matching mode (partial or complete) is requested
++that was not compiled.
+ </p>
+ <p>
+ When you call <b>pcre2_match()</b>, as well as testing for invalid options, a
+diff --git a/doc/pcre2.txt b/doc/pcre2.txt
+index 693908ee..cc316178 100644
+--- a/doc/pcre2.txt
++++ b/doc/pcre2.txt
+@@ -6176,11 +6176,11 @@ JIT FAST PATH API
+ The fast path function is called pcre2_jit_match(), and it takes ex-
+ actly the same arguments as pcre2_match(). However, the subject string
+ must be specified with a length; PCRE2_ZERO_TERMINATED is not sup-
+- ported. Unsupported option bits (for example, PCRE2_ANCHORED and
+- PCRE2_ENDANCHORED) are ignored, as is the PCRE2_NO_JIT option. The re-
+- turn values are also the same as for pcre2_match(), plus PCRE2_ER-
+- ROR_JIT_BADOPTION if a matching mode (partial or complete) is requested
+- that was not compiled.
++ ported. Unsupported option bits (for example, PCRE2_ANCHORED, PCRE2_EN-
++ DANCHORED, and PCRE2_COPY_MATCHED_SUBJECT) are ignored, as is the
++ PCRE2_NO_JIT option. The return values are also the same as for
++ pcre2_match(), plus PCRE2_ERROR_JIT_BADOPTION if a matching mode (par-
++ tial or complete) is requested that was not compiled.
+
+ When you call pcre2_match(), as well as testing for invalid options, a
+ number of other sanity checks are performed on the arguments. For exam-
+diff --git a/doc/pcre2jit.3 b/doc/pcre2jit.3
+index 95451b56..729d898f 100644
+--- a/doc/pcre2jit.3
++++ b/doc/pcre2jit.3
+@@ -444,10 +444,11 @@ processed by \fBpcre2_jit_compile()\fP).
+ The fast path function is called \fBpcre2_jit_match()\fP, and it takes exactly
+ the same arguments as \fBpcre2_match()\fP. However, the subject string must be
+ specified with a length; PCRE2_ZERO_TERMINATED is not supported. Unsupported
+-option bits (for example, PCRE2_ANCHORED and PCRE2_ENDANCHORED) are ignored, as
+-is the PCRE2_NO_JIT option. The return values are also the same as for
+-\fBpcre2_match()\fP, plus PCRE2_ERROR_JIT_BADOPTION if a matching mode (partial
+-or complete) is requested that was not compiled.
++option bits (for example, PCRE2_ANCHORED, PCRE2_ENDANCHORED, and
++PCRE2_COPY_MATCHED_SUBJECT) are ignored, as is the PCRE2_NO_JIT option. The
++return values are also the same as for \fBpcre2_match()\fP, plus
++PCRE2_ERROR_JIT_BADOPTION if a matching mode (partial or complete) is requested
++that was not compiled.
+ .P
+ When you call \fBpcre2_match()\fP, as well as testing for invalid options, a
+ number of other sanity checks are performed on the arguments. For example, if
+diff --git a/src/pcre2_jit_match_inc.h b/src/pcre2_jit_match_inc.h
+index 32d4c8a5..4163cf61 100644
+--- a/src/pcre2_jit_match_inc.h
++++ b/src/pcre2_jit_match_inc.h
+@@ -125,6 +125,16 @@ else if ((options & PCRE2_PARTIAL_SOFT) != 0)
+ if (functions == NULL || functions->executable_funcs[index] == NULL)
+ return match_data->rc = PCRE2_ERROR_JIT_BADOPTION;
+
++/* If the match data block was previously used with PCRE2_COPY_MATCHED_SUBJECT,
++free the memory that was obtained. */
++
++if ((match_data->flags & PCRE2_MD_COPIED_SUBJECT) != 0)
++ {
++ match_data->memctl.free((void *)match_data->subject,
++ match_data->memctl.memory_data);
++ match_data->flags &= ~PCRE2_MD_COPIED_SUBJECT;
++ }
++
+ /* Sanity checks should be handled by pcre2_match. */
+ arguments.str = subject + start_offset;
+ arguments.begin = subject;
+diff --git a/src/pcre2test_inc.h b/src/pcre2test_inc.h
+index 5d282435..a74e3368 100644
+--- a/src/pcre2test_inc.h
++++ b/src/pcre2test_inc.h
+@@ -5171,20 +5171,28 @@ for (gmatched = 0;; gmatched++)
+ /* If PCRE2_COPY_MATCHED_SUBJECT was set, check that things are as they
+ should be, but not for fast JIT, where it isn't supported. */
+
+- if ((dat_datctl.options & PCRE2_COPY_MATCHED_SUBJECT) != 0 &&
+- (pat_patctl.control & CTL_JITFAST) == 0)
++ if ((dat_datctl.options & PCRE2_COPY_MATCHED_SUBJECT) != 0)
+ {
+- if ((match_data->flags & PCRE2_MD_COPIED_SUBJECT) == 0)
+- cfprintf(clr_test_error, outfile,
+- "** PCRE2 error: flag not set after copy_matched_subject\n");
++ if ((pat_patctl.control & CTL_JITFAST) != 0)
++ {
++ if ((match_data->flags & PCRE2_MD_COPIED_SUBJECT) != 0)
++ cfprintf(clr_test_error, outfile,
++ "** PCRE2 error: flag set after unsupported copy_matched_subject\n");
++ }
++ else
++ {
++ if ((match_data->flags & PCRE2_MD_COPIED_SUBJECT) == 0)
++ cfprintf(clr_test_error, outfile,
++ "** PCRE2 error: flag not set after copy_matched_subject\n");
+
+- if (match_data->subject == pp)
+- cfprintf(clr_test_error, outfile,
+- "** PCRE2 error: copy_matched_subject has not copied\n");
++ if (match_data->subject == pp)
++ cfprintf(clr_test_error, outfile,
++ "** PCRE2 error: copy_matched_subject has not copied\n");
+
+- if (memcmp(match_data->subject, pp, ulen) != 0)
+- cfprintf(clr_test_error, outfile,
+- "** PCRE2 error: copy_matched_subject mismatch\n");
++ if (memcmp(match_data->subject, pp, ulen) != 0)
++ cfprintf(clr_test_error, outfile,
++ "** PCRE2 error: copy_matched_subject mismatch\n");
++ }
+ }
+
+ /* If this is not the first time round a global loop, check that the
+@@ -5661,6 +5669,9 @@ pcre2_match_context *test_dat_context = NULL, *test_dat_context_copy = NULL;
+ pcre2_convert_context *test_con_context = NULL, *test_con_context_copy = NULL;
+ pcre2_match_data *test_match_data = NULL;
+ pcre2_code *test_compiled_code = NULL;
++#ifdef SUPPORT_JIT
++BOOL test_compiled_with_jit = FALSE;
++#endif
+ PCRE2_UCHAR pattern[] = { CHAR_A, CHAR_B, CHAR_C, 0 };
+ PCRE2_UCHAR callout_int_pattern[] = {
+ CHAR_LEFT_PARENTHESIS, CHAR_QUESTION_MARK, CHAR_C, CHAR_RIGHT_PARENTHESIS, 0 };
+@@ -5965,11 +5976,38 @@ ASSERT(rc == 0 && sizeval == 0, "pcre2_pattern_info(JIT)");
+
+ if (pcre2_jit_compile(test_compiled_code, PCRE2_JIT_COMPLETE) == 0)
+ {
++ test_compiled_with_jit = TRUE;
++
+ rc = pcre2_pattern_info(test_compiled_code, PCRE2_INFO_JITSIZE, &sizeval);
+ ASSERT(rc == 0 && sizeval > 0, "pcre2_pattern_info(JIT after compile)");
+ }
+ #endif
+
++/* ----------------------- Matching functions ------------------------------ */
++
++#ifdef SUPPORT_JIT
++
++/* Check that fast JIT releases a copied subject when reusing match data. */
++if (test_compiled_with_jit)
++ {
++ test_match_data = pcre2_match_data_create_from_pattern(test_compiled_code,
++ test_gen_context);
++ ASSERT(test_match_data != NULL, "pcre2_match_data_create_from_pattern(JIT)");
++
++ rc = pcre2_match(test_compiled_code, pattern, 3, 0,
++ PCRE2_COPY_MATCHED_SUBJECT, test_match_data, NULL);
++ ASSERT(rc == 1, "pcre2_match(COPY_MATCHED_SUBJECT)");
++
++ rc = pcre2_jit_match(test_compiled_code, subject_abcz, 4, 0, 0,
++ test_match_data, NULL);
++ ASSERT(rc == 1, "pcre2_jit_match(reused match data)");
++
++ pcre2_match_data_free(test_match_data);
++ test_match_data = NULL;
++ }
++
++#endif
++
+ /* ----------------------- POSIX functions --------------------------------- */
+
+ #if PCRE2_CODE_UNIT_WIDTH == 8
+diff --git a/testdata/testinput17 b/testdata/testinput17
+index 08fd72e0..9d728965 100644
+--- a/testdata/testinput17
++++ b/testdata/testinput17
+@@ -298,6 +298,7 @@
+
+ /abc/jitfast
+ abc
++ abc\=copy_matched_subject
+ abc\=no_jit
+
+ # ----
+diff --git a/testdata/testoutput17 b/testdata/testoutput17
+index 6d550084..773ec18a 100644
+--- a/testdata/testoutput17
++++ b/testdata/testoutput17
+@@ -542,6 +542,8 @@ Failed: error -47: match limit exceeded
+
+ /abc/jitfast
+ abc
++ 0: abc (JIT)
++ abc\=copy_matched_subject
+ 0: abc (JIT)
+ abc\=no_jit
+ 0: abc (JIT)
@@ -15,6 +15,7 @@ LIC_FILES_CHKSUM = "file://LICENCE.md;md5=6720bf3bcff57543b915c2b22e526df0 \
SRC_URI = "${GITHUB_BASE_URI}/download/pcre2-${PV}/pcre2-${PV}.tar.bz2 \
file://run-ptest \
file://CVE-2026-89162.patch \
+ file://CVE-2026-89161.patch \
"
GITHUB_BASE_URI = "https://github.com/PCRE2Project/pcre2/releases"