Commit 1dda70872c for perl

commit 1dda70872c82ca72322d916322cb5a85b54abbc8
Author: Yves Orton <demerphq@gmail.com>
Date:   Sun Oct 18 13:45:59 2026 +0200

    regex: reject trie words ending inside a character fold

    Under /iu, s|zoo must not match sharp-s by consuming only the first s of
    its fold. When finding the end of a trie word, reject it if part of the
    input character's fold is left over.

    Try the next word in alternative order before running a continuation. A
    longer word may consume the whole fold and match.

diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 10aa12472e..310d0b9108 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -431,6 +431,12 @@ manager will later use a regex to expand these into links.

 =item *

+A case-insensitive regex trie no longer accepts a word ending partway through
+an input character's fold. For example, C</s|zoo/iu> no longer matches a
+sharp-s, whose full fold is C<ss>. Later alternatives are tried in order.
+
+=item *
+
 A folded regex trie now updates its minimum and maximum lengths independently.
 An alternative which changes both bounds could previously cause a valid
 match to be missed or an assertion failure during Aho-Corasick scanning.
diff --git a/regexec.c b/regexec.c
index 9c0ef28a8b..02c986a8a5 100644
--- a/regexec.c
+++ b/regexec.c
@@ -7479,6 +7479,7 @@ S_regmatch(pTHX_ regmatch_info *reginfo, char *startpos, regnode *prog)
                     CAPTURE_CLEAR(ST.before_paren+1, ST.after_paren, "TRIE_next_fail");
                 }
             }
+          trie_next_word:
             if (!--ST.accepted) {
                 DEBUG_EXECUTE_r({
                     re_exec_indentf("%sTRIE failed...%s\n",
@@ -7550,13 +7551,19 @@ S_regmatch(pTHX_ regmatch_info *reginfo, char *startpos, regnode *prog)
                         uvc = to_uni_fold(uvc, foldbuf, &foldlen);
                         uscan = foldbuf;
                         while (foldlen) {
-                            if (!--chars)
-                                break;
-                            /* The folder produces valid UTF-8.  Only its
+                            /* The folder produces valid UTF-8. Only its
                              * character boundaries are needed here. */
                             len = UTF8SKIP(uscan);
                             uscan += len;
                             foldlen -= len;
+                            if (!--chars) {
+                                /* A word ending inside this input
+                                 * character's fold is not a match. Try
+                                 * the next word before running its tail. */
+                                if (foldlen)
+                                    goto trie_next_word;
+                                break;
+                            }
                         }
                     }
                 }
diff --git a/t/re/pat.t b/t/re/pat.t
index 8e33d4104a..c7c60913dd 100644
--- a/t/re/pat.t
+++ b/t/re/pat.t
@@ -28,7 +28,7 @@ skip_all_without_unicode_tables();
 my $has_locales = locales_enabled('LC_CTYPE');
 my $utf8_locale = find_utf8_ctype_locale();

-plan tests => 2630 + 6 * $Config{uvsize};  # Update when adding/deleting tests.
+plan tests => 2726 + 6 * $Config{uvsize};  # Update when adding/deleting tests.

 run_tests() unless caller;

@@ -208,6 +208,43 @@ sub run_tests {
         }
     }

+    {
+        my @cases = (
+            ['s|zoo', "\x{df}!"],
+            ['s([!]?)|ss([!]?)', "\x{df}!"],
+            ['f([!]?)|ffi([!]?)', "\x{fb03}!"],
+            ['ff([!]?)|ffi([!]?)', "\x{fb03}!"],
+            ['ss([!]?)|s([!]?)', "\x{df}!"],
+            ["\x{3b9}\x{308}([!]?)|\x{3b9}\x{308}\x{301}([!]?)",
+             "\x{390}!"],
+        );
+        for my $case (@cases) {
+            for my $charset ('u', 'a', 'aa', 'd') {
+                for my $pattern_utf8 (0, 1) {
+                    my $source = "\\A(?i$charset:$case->[0])";
+                    utf8::upgrade($source) if $pattern_utf8;
+                    my ($plain, $trie);
+                    { local ${^RE_TRIE_MAXBUF} = -1;
+                      $plain = qr/$source(?#fold endpoints branches)/; }
+                    { local ${^RE_TRIE_MAXBUF} = 65536;
+                      $trie = qr/$source(?#fold endpoints trie)/; }
+                    for my $target_utf8 (0, 1) {
+                        my $target = $case->[1];
+                        utf8::upgrade($target) if $target_utf8;
+                        my @result;
+                        for my $re ($plain, $trie) {
+                            push @result, $target =~ $re
+                                ? join(':', map { defined $_ ? $_ : '-' } @-, @+)
+                                : 'no match';
+                        }
+                        is($result[1], $result[0],
+                           "trie acceptance ends at a complete fold: /$charset, pattern_utf8=$pattern_utf8, target_utf8=$target_utf8");
+                    }
+                }
+            }
+        }
+    }
+
     {
         my @folded_backtracks = (
             ['ss', "\x{df}"],