]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
unicode_case.c: defend against truncated UTF8.
authorJeff Davis <jdavis@postgresql.org>
Tue, 7 Jul 2026 20:35:23 +0000 (13:35 -0700)
committerJeff Davis <jdavis@postgresql.org>
Tue, 7 Jul 2026 20:35:23 +0000 (13:35 -0700)
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/c355354e6c3f4a7aafb047361b73db247260fca0.camel@j-davis.com
Backpatch-through: 17

src/backend/utils/adt/formatting.c
src/common/unicode/case_test.c
src/common/unicode_case.c

index acc1e285b03338c439b2a4624f9e8f7b779dfc2f..fcfe6ed4ba86cc82d601c81f2632cbe88d884600 100644 (file)
@@ -1945,21 +1945,33 @@ initcap_wbnext(void *state)
        while (wbstate->offset < wbstate->len &&
                   wbstate->str[wbstate->offset] != '\0')
        {
-               pg_wchar        u = utf8_to_unicode((unsigned char *) wbstate->str +
+               int                     ulen = pg_utf_mblen((const unsigned char *) wbstate->str +
                                                                                wbstate->offset);
-               bool            curr_alnum = pg_u_isalnum(u, true);
+               pg_wchar        u;
+               bool            curr_alnum;
+               size_t          prev_offset = wbstate->offset;
 
-               if (!wbstate->init || curr_alnum != wbstate->prev_alnum)
+               /* invalid UTF8 */
+               if (wbstate->offset + ulen > wbstate->len)
                {
-                       size_t          prev_offset = wbstate->offset;
+                       wbstate->init = true;
+                       wbstate->offset = wbstate->len;
+                       return prev_offset;
+               }
 
+               u = utf8_to_unicode((const unsigned char *) wbstate->str +
+                                                       wbstate->offset);
+               curr_alnum = pg_u_isalnum(u, true);
+
+               if (!wbstate->init || curr_alnum != wbstate->prev_alnum)
+               {
                        wbstate->init = true;
-                       wbstate->offset += unicode_utf8len(u);
+                       wbstate->offset += ulen;
                        wbstate->prev_alnum = curr_alnum;
                        return prev_offset;
                }
 
-               wbstate->offset += unicode_utf8len(u);
+               wbstate->offset += ulen;
        }
 
        return wbstate->len;
index de92811ec72612f99e2d21994d86b1776e937ee0..ea6a56a4bf3fffac6e45c8a2fdf07c25295e4bd9 100644 (file)
@@ -169,6 +169,8 @@ test_strlower(const char *test_string, const char *expected)
 static void
 test_convert_case()
 {
+       size_t          needed;
+
        /* test string with no case changes */
        test_strlower("√∞", "√∞");
        /* test string with case changes */
@@ -176,6 +178,13 @@ test_convert_case()
        /* test string with case changes and byte length changes */
        test_strlower("ȺȺȺ", "ⱥⱥⱥ");
 
+       /* invalid UTF8: truncated multibyte sequence */
+       needed = unicode_strlower(NULL, 0, "abc\xCE", 4);
+       Assert(needed == 3);
+       /* invalid UTF8: invalid byte */
+       needed = unicode_strlower(NULL, 0, "abc\xF8xyz", 7);
+       Assert(needed == 3);
+
        printf("case_test: convert_case: success\n");
 }
 
index 291e256e639af7066e4c75004d809279d74672b7..ecc2a07c2251b12295dd79e9f96ad6bff3de4262 100644 (file)
@@ -126,6 +126,22 @@ unicode_strupper(char *dst, size_t dstsize, const char *src, ssize_t srclen)
        return convert_case(dst, dstsize, src, srclen, CaseUpper, NULL, NULL);
 }
 
+/* local version of pg_utf_mblen() to be inlinable */
+static int
+utf8_mblen(const unsigned char *s)
+{
+       if ((*s & 0x80) == 0)
+               return 1;
+       else if ((*s & 0xe0) == 0xc0)
+               return 2;
+       else if ((*s & 0xf0) == 0xe0)
+               return 3;
+       else if ((*s & 0xf8) == 0xf0)
+               return 4;
+       else
+               return -1;
+}
+
 /*
  * If str_casekind is CaseLower or CaseUpper, map each character in the string
  * for which a mapping is available.
@@ -152,11 +168,18 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen,
                Assert(boundary == 0);  /* start of text is always a boundary */
        }
 
-       while ((srclen < 0 || srcoff < srclen) && src[srcoff] != '\0')
+       srclen = (srclen < 0) ? strlen(src) : srclen;
+       while (srcoff < srclen && src[srcoff] != '\0')
        {
-               pg_wchar        u1 = utf8_to_unicode((unsigned char *) src + srcoff);
-               int                     u1len = unicode_utf8len(u1);
-               const pg_case_map *casemap = find_case_map(u1);
+               int                     u1len = utf8_mblen((const unsigned char *) src + srcoff);
+               pg_wchar        u1;
+               const pg_case_map *casemap;
+
+               /* invalid UTF8 */
+               if (u1len < 0 || srcoff + u1len > srclen)
+                       break;
+
+               u1 = utf8_to_unicode((const unsigned char *) src + srcoff);
 
                if (str_casekind == CaseTitle)
                {
@@ -169,6 +192,8 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen,
                                chr_casekind = CaseLower;
                }
 
+               casemap = find_case_map(u1);
+
                /* perform mapping, update result_len, and write to dst */
                if (casemap)
                {