From 12958a533f2115ac198f3ddd62a902d13afd7b0a Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:27:04 -0500 Subject: [PATCH 1/2] Return the listed captures when (*ACCEPT) ends a called group When a group called with a capture list, such as (?&r(2)), ended with (*ACCEPT), the interpreter went back to the caller's frame and moved past the OP_RECURSE, but left the following OP_CREF list in place. Matching then continued at OP_CREF, which hit the unreachable-opcode default: an assertion failure in debug builds and PCRE2_ERROR_INTERNAL otherwise. The callee's captures were also dropped. The ACCEPT path now does what the end of a recursed group already does: if the OP_RECURSE has a capture list, recurse_update_offsets() builds the caller's captures, keeping the listed groups from the callee, and moves past the list. The result is copied into the caller's frame before matching continues there. Fixes #1045 --- src/pcre2_match.c | 18 +++++++++++++++-- testdata/testinput2 | 26 ++++++++++++++++++++++++ testdata/testoutput2 | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/pcre2_match.c b/src/pcre2_match.c index e4f8f551c..e70e89a12 100644 --- a/src/pcre2_match.c +++ b/src/pcre2_match.c @@ -1005,13 +1005,27 @@ match(PCRE2_SPTR start_eptr, PCRE2_SPTR start_ecode, uint16_t top_bracket, PCRE2 /* N is now the frame of the recursion; the previous frame is at the OP_RECURSE position. Go back there, copying the current subject position and mark, and the start_match position (\K might have changed it), and - then move on past the OP_RECURSE. */ + then move on past the OP_RECURSE. + + If the OP_RECURSE is followed by a list of capture groups to return, + build the caller's captures in the current frame, as is done when the + end of the recursion is reached, and copy them into the previous frame. + This also moves past the OP_CREF list. */ P->eptr = Feptr; P->mark = Fmark; P->start_match = Fstart_match; + + Fecode = P->ecode + 1 + LINK_SIZE; + if (*Fecode == OP_CREF) + { + recurse_update_offsets(F, P); + memcpy(P->ovector, F->ovector, Foffset_top * sizeof(PCRE2_SIZE)); + P->offset_top = Foffset_top; + } + P->ecode = Fecode; + F = P; - Fecode += 1 + LINK_SIZE; continue; } diff --git a/testdata/testinput2 b/testdata/testinput2 index 812d0b8e6..cc2703f1f 100644 --- a/testdata/testinput2 +++ b/testdata/testinput2 @@ -8275,6 +8275,32 @@ a)"xI /(?(DEFINE)((Sat)(urday)))(?1(2,3)),\2,\3/ Saturday,Sat,urday +# (*ACCEPT) in a called group must return the listed captures to the caller + +/(?&r(2))b(?(DEFINE)(?(a)(*ACCEPT)x))/ + ab + +/(a)(?2(3))\1\3(?(DEFINE)(b(c)(*ACCEPT)x))/ + abcac +\= Expect no match + abcab + +/^(?1(2,3))(?(DEFINE)((a)(*ACCEPT)(b)))/ + ab + +/(?1(2))(?1(3))(?(DEFINE)((a)(*ACCEPT)|(b)))!/ + ab! + +/(?(R)(a)(*ACCEPT)x|(?R(1))b\1)/ + aba +\= Expect no match + abx + +/(?1(2))c(?(DEFINE)((a)(*ACCEPT)|(ab)))/ + ac +\= Expect no match + abc + # -------------- # Test that a memory leak has been fixed diff --git a/testdata/testoutput2 b/testdata/testoutput2 index 6ffa12c6c..ad91bce87 100644 --- a/testdata/testoutput2 +++ b/testdata/testoutput2 @@ -23617,6 +23617,54 @@ Failed: error 217 at offset 27: expected capture group number or name 2: Sat 3: urday +# (*ACCEPT) in a called group must return the listed captures to the caller + +/(?&r(2))b(?(DEFINE)(?(a)(*ACCEPT)x))/ + ab + 0: ab + 1: + 2: a + +/(a)(?2(3))\1\3(?(DEFINE)(b(c)(*ACCEPT)x))/ + abcac + 0: abcac + 1: a + 2: + 3: c +\= Expect no match + abcab +No match + +/^(?1(2,3))(?(DEFINE)((a)(*ACCEPT)(b)))/ + ab + 0: a + 1: + 2: a + +/(?1(2))(?1(3))(?(DEFINE)((a)(*ACCEPT)|(b)))!/ + ab! + 0: ab! + 1: + 2: a + 3: b + +/(?(R)(a)(*ACCEPT)x|(?R(1))b\1)/ + aba + 0: aba + 1: a +\= Expect no match + abx +No match + +/(?1(2))c(?(DEFINE)((a)(*ACCEPT)|(ab)))/ + ac + 0: ac + 1: + 2: a +\= Expect no match + abc +No match + # -------------- # Test that a memory leak has been fixed From a1c7c8d449b9ebdcc14687d9803ef2253d58fc2f Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:15:26 -0500 Subject: [PATCH 2/2] Close open groups at (*ACCEPT) inside a recursion OP_CLOSE was skipped inside recursions because captures used not to be passed out of them. With capture lists they can be, so close the groups that are open at the (*ACCEPT), stopping the frame search at the recursion's frame so that only groups opened within the recursion are closed. --- src/pcre2_match.c | 38 +++++++++++++++++++++----------------- testdata/testinput2 | 25 +++++++++++++++++++++++++ testdata/testoutput2 | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/pcre2_match.c b/src/pcre2_match.c index e70e89a12..72635e6a1 100644 --- a/src/pcre2_match.c +++ b/src/pcre2_match.c @@ -932,28 +932,32 @@ match(PCRE2_SPTR start_eptr, PCRE2_SPTR start_ecode, uint16_t top_bracket, PCRE2 where we know the starting frame is at the top of the chained frames, in this case we have to search back for the relevant frame in case other types of group that use chained frames have intervened. Multiple OP_CLOSEs always - come innermost first, which matches the chain order. We can ignore this in - a recursion, because captures are not passed out of recursions. */ + come innermost first, which matches the chain order. + + In a recursion, the closed groups may be returned to the caller by a + capture list. Only groups that were opened within the recursion are closed, + so the search stops at the recursion's frame. */ case OP_CLOSE: - if (Fcurrent_recurse == RECURSE_UNSET) + number = GET2(Fecode, 1); + offset = Flast_group_offset; + for (;;) { - number = GET2(Fecode, 1); - offset = Flast_group_offset; - for (;;) - { - /* Corrupted heapframes?. Trigger an assert and return an error */ - PCRE2_ASSERT(offset != PCRE2_UNSET); - if (offset == PCRE2_UNSET) - return PCRE2_ERROR_INTERNAL; + /* Corrupted heapframes?. Trigger an assert and return an error */ + PCRE2_ASSERT(offset != PCRE2_UNSET); + if (offset == PCRE2_UNSET) + return PCRE2_ERROR_INTERNAL; - N = (heapframe *)((char *)match_data->heapframes + offset); - P = (heapframe *)((char *)N - frame_size); - if (N->group_frame_type == (GF_CAPTURE | number)) - break; - offset = P->last_group_offset; - } + N = (heapframe *)((char *)match_data->heapframes + offset); + P = (heapframe *)((char *)N - frame_size); + if (N->group_frame_type == (GF_CAPTURE | number) || + GF_IDMASK(N->group_frame_type) == GF_RECURSE) + break; + offset = P->last_group_offset; + } + if (N->group_frame_type == (GF_CAPTURE | number)) + { offset = (number << 1) - 2; Fcapture_last = number; Fovector[offset] = P->eptr - mb->start_subject; diff --git a/testdata/testinput2 b/testdata/testinput2 index cc2703f1f..6fb189952 100644 --- a/testdata/testinput2 +++ b/testdata/testinput2 @@ -8301,6 +8301,31 @@ a)"xI \= Expect no match abc +# Groups that are still open at (*ACCEPT) in a called group are closed before +# the listed captures are returned, but groups opened outside the called group +# are not. JIT does not yet close the open groups. + +/(*NO_JIT)^(?&r(2))b\2$(?(DEFINE)(?(a(*ACCEPT)x)))/ + aba +\= Expect no match + abb + +/(*NO_JIT)^(?&r(2,3))\2\3$(?(DEFINE)(?(a(b(*ACCEPT)c))))/ + ababb + +/(*NO_JIT)^(?&r(3))\3(?(2)x|y)$(?(DEFINE)(?(a(b(*ACCEPT)c))))/ + abby +\= Expect no match + abbx + +/(*NO_JIT)(?(R)(a(*ACCEPT)x)|(?R(1))b\1)/ + aba + +/^(x(?a(*ACCEPT)b){0}(?&r(1))(?(1)y|z))/ + xaz +\= Expect no match + xay + # -------------- # Test that a memory leak has been fixed diff --git a/testdata/testoutput2 b/testdata/testoutput2 index ad91bce87..734ec5de8 100644 --- a/testdata/testoutput2 +++ b/testdata/testoutput2 @@ -23665,6 +23665,49 @@ No match abc No match +# Groups that are still open at (*ACCEPT) in a called group are closed before +# the listed captures are returned, but groups opened outside the called group +# are not. JIT does not yet close the open groups. + +/(*NO_JIT)^(?&r(2))b\2$(?(DEFINE)(?(a(*ACCEPT)x)))/ + aba + 0: aba + 1: + 2: a +\= Expect no match + abb +No match + +/(*NO_JIT)^(?&r(2,3))\2\3$(?(DEFINE)(?(a(b(*ACCEPT)c))))/ + ababb + 0: ababb + 1: + 2: ab + 3: b + +/(*NO_JIT)^(?&r(3))\3(?(2)x|y)$(?(DEFINE)(?(a(b(*ACCEPT)c))))/ + abby + 0: abby + 1: + 2: + 3: b +\= Expect no match + abbx +No match + +/(*NO_JIT)(?(R)(a(*ACCEPT)x)|(?R(1))b\1)/ + aba + 0: aba + 1: a + +/^(x(?a(*ACCEPT)b){0}(?&r(1))(?(1)y|z))/ + xaz + 0: xaz + 1: xaz +\= Expect no match + xay +No match + # -------------- # Test that a memory leak has been fixed