From 5e78ebca57b3630c1230d348263fddfeaaf7678e Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Tue, 7 Jul 2026 13:35:23 -0700 Subject: [PATCH] unicode_case.c: defend against truncated UTF8. Reviewed-by: Chao Li 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; + + /* invalid UTF8 */ + if (wbstate->offset + ulen > wbstate->len) + { + 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) { - size_t prev_offset = wbstate->offset; - 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) {