Commit 7fb635f95f for perl

commit 7fb635f95f08105b5e84dcec3eab228b4a9dee5c
Author: Alin Iacob <alinbsp@gmail.com>
Date:   Thu Sep 24 13:04:24 2026 +0300

    sv.c: restore UTF-8 offset caching

    The midway paths left canonical_position false, so substr() wasn't
    updating the UTF-8 position cache.

    Set at_end for the end position so it only updates the length cache.
    Check for out-of-range offsets before calculating the backward
    distance.

    Based on a patch by kaz-utashiro.

    Fixes #24531

diff --git a/sv.c b/sv.c
index 902c4f9c15..71458822ed 100644
--- a/sv.c
+++ b/sv.c
@@ -8676,6 +8676,10 @@ S_sv_pos_u2b_midway(const U8 *const start, const U8 *send,
 {
     PERL_ARGS_ASSERT_SV_POS_U2B_MIDWAY;

+    /* Avoid unsigned underflow for offsets beyond the end. */
+    if (uoffset >= uend)
+        return send - start;
+
     STRLEN backw = uend - uoffset;

     if (uoffset < 2 * backw) {
@@ -8745,6 +8749,8 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
                 }
                 if ((*mgp)->mg_len != -1) {
                     /* And we know the end too.  */
+                    canonical_position = uoffset <= (STRLEN)(*mgp)->mg_len;
+                    at_end = uoffset >= (STRLEN)(*mgp)->mg_len;
                     boffset = boffset0
                         + sv_pos_u2b_midway(start + boffset0, send,
                                               uoffset - uoffset0,
@@ -8766,12 +8772,14 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
                     boffset0 = cache[3];
                 }

+                canonical_position = TRUE;
                 boffset = boffset0
                     + sv_pos_u2b_midway(start + boffset0,
                                           start + cache[1],
                                           uoffset - uoffset0,
                                           cache[0] - uoffset0);
             } else {
+                canonical_position = TRUE;
                 boffset = boffset0
                     + sv_pos_u2b_midway(start + boffset0,
                                           start + cache[3],
@@ -8784,6 +8792,8 @@ S_sv_pos_u2b_cached(pTHX_ SV *const sv, MAGIC **const mgp, const U8 *const start
             /* If we can take advantage of a passed in offset, do so.  */
             /* In fact, offset0 is either 0, or less than offset, so don't
                need to worry about the other possibility.  */
+            canonical_position = uoffset <= (STRLEN)(*mgp)->mg_len;
+            at_end = uoffset >= (STRLEN)(*mgp)->mg_len;
             boffset = boffset0
                 + sv_pos_u2b_midway(start + boffset0, send,
                                       uoffset - uoffset0,
diff --git a/t/op/index.t b/t/op/index.t
index a218848851..2904ee0251 100644
--- a/t/op/index.t
+++ b/t/op/index.t
@@ -8,7 +8,7 @@ BEGIN {
 }

 use strict;
-plan( tests => 415 );
+plan( tests => 424 );

 run_tests() unless caller;

@@ -366,4 +366,16 @@ EOS
         is(index($s, "", $len+1), 3, 'Overlong index doesn\'t confuse utf8 cache');
     }

+    {
+        local ${^UTF8CACHE} = 1; # enable cache, disable debugging
+        for my $text ("", "abc", "\x{100}" x 3) {
+            my $s = $text;
+            utf8::upgrade($s);
+            my $len = length($s);
+            is(index($s, "", $len), $len, 'index at the cached end');
+            is(index($s, "", $len+1), $len, 'index beyond the cached end');
+            is(rindex($s, "", $len+1), $len, 'rindex beyond the cached end');
+        }
+    }
+
 } # end of sub run_tests
diff --git a/t/perf/speed.t b/t/perf/speed.t
index 7df4b4e9a9..ef456ec1b1 100644
--- a/t/perf/speed.t
+++ b/t/perf/speed.t
@@ -24,7 +24,7 @@ use 5.010;

 $| = 1;

-plan tests => 1;
+plan tests => 2;

 watchdog(60);

@@ -42,6 +42,15 @@ SKIP: {
     pass("COW 1Mb strings");
 }

+{
+    # GH #24531: substr wasn't updating the UTF-8 position cache.
+    local ${^UTF8CACHE} = 1; # enable cache, disable debugging
+    my $s = "\x{100}" x 1_000_000;
+    my $part;
+    $part = substr($s, 500_000, 10) for 1..100_000;
+    is($part, "\x{100}" x 10, 'repeated substr on a UTF-8 string');
+}
+
 watchdog(0);

 1;