postgres.git / summary / log / commit / refs
commit 5e78ebca57b3630c1230d348263fddfeaaf7678e
Author: Jeff Davis <jdavis@postgresql.org>
Date: Tue Jul 07 20:35:23 2026 +0000
First released in: 17.11, 18.6
unicode_case.c: defend against truncated UTF8.
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 | 24 ++++++++++++++++++------
src/common/unicode/case_test.c | 9 +++++++++
src/common/unicode_case.c | 33 +++++++++++++++++++++++++++++----
3 files changed, 56 insertions(+), 10 deletions(-)
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index acc1e285b03..fcfe6ed4ba8 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -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;
diff --git a/src/common/unicode/case_test.c b/src/common/unicode/case_test.c
index de92811ec72..ea6a56a4bf3 100644
--- a/src/common/unicode/case_test.c
+++ b/src/common/unicode/case_test.c
@@ -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");
}
diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c
index 291e256e639..ecc2a07c225 100644
--- a/src/common/unicode_case.c
+++ b/src/common/unicode_case.c
@@ -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)
{
[parent: 177a2a2e396c]