From: Luca Boccassi Date: Thu, 9 Jul 2026 10:15:32 +0000 (+0100) Subject: string-util: add upper bound to ellipsize_mem UTF-8 walks X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=66600e15c913171f20f8da3f32467bf177395d62;p=thirdparty%2Fsystemd.git string-util: add upper bound to ellipsize_mem UTF-8 walks Validate UTF-8 characters against the caller-provided byte slice. This stops truncated sequences from borrowing bytes past old_length. Follow-up for e69cc9eb36fd6e76710b4d5f4bb7013980fb5174 --- diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 1dc90d538ad..047a58ade57 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -357,6 +357,33 @@ static char *ascii_ellipsize_mem(const char *s, size_t old_length, size_t new_le return t; } +/* Walk backwards from 'end' (exclusive) to the start of the last complete UTF-8 character, without + * descending below 'start', validate it and return a pointer to its first byte, optionally decoding it + * into *ret_c. Returns NULL on a truncated or otherwise malformed sequence. */ +static const char* find_previous_unichar(const char *start, const char *end, char32_t *ret_c) { + const char *p; + int r; + + assert(start); + assert(end); + assert(end > start); + + for (p = end; p > start; ) { + p--; + if (((uint8_t) *p & 0xc0) != 0x80) /* Found a non-continuation byte, i.e. a character start. */ + break; + } + + r = utf8_encoded_valid_unichar(p, end - p); + if (r < 0 || p + r != end) + return NULL; + + if (ret_c) + assert_se(utf8_encoded_to_unichar(p, ret_c) == r); + + return p; +} + char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigned percent) { size_t x, k, len, len2; const char *i, *j; @@ -400,11 +427,13 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne continue; /* ANSI sequences don't take up any space in output */ } - char32_t c; - r = utf8_encoded_to_unichar(i, &c); + r = utf8_encoded_valid_unichar(i, s + old_length - i); if (r < 0) return NULL; + char32_t c; + assert_se(utf8_encoded_to_unichar(i, &c) == r); + int w = unichar_iswide(c) ? 2 : 1; if (k + w > x) break; @@ -431,9 +460,9 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne continue; } - tt = utf8_prev_char(t); - r = utf8_encoded_to_unichar(tt, &c); - if (r < 0) + /* Find the previous complete UTF-8 character inside the retained suffix. */ + tt = find_previous_unichar(i, t, &c); + if (!tt) return NULL; w = unichar_iswide(c) ? 2 : 1; @@ -451,11 +480,21 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne if (k >= new_length) { /* Make space for ellipsis, if required and possible. We know that the edge character is not * part of an ANSI sequence (because then we'd skip it). If the last character we looked at - * was wide, we don't need to make space. */ - if (j < s + old_length) - j = utf8_next_char(j); - else if (i > s) - i = utf8_prev_char(i); + * was wide, we don't need to make space. + * Move the edge by one complete UTF-8 character within the input slice. */ + if (j < s + old_length) { + r = utf8_encoded_valid_unichar(j, s + old_length - j); + if (r < 0) + return NULL; + + j += r; + } else if (i > s) { + const char *tt = find_previous_unichar(s, i, NULL); + if (!tt) + return NULL; + + i = tt; + } } len = i - s; diff --git a/src/test/test-ellipsize.c b/src/test/test-ellipsize.c index 2efb123ac2b..3d47cfebe26 100644 --- a/src/test/test-ellipsize.c +++ b/src/test/test-ellipsize.c @@ -75,6 +75,12 @@ TEST(ellipsize_mem) { test_ellipsize_mem_one(s, l, k); } +TEST(ellipsize_mem_truncated_utf8) { + static const char s[] = { 'a', '\xf0', '\x80', '\x80', '\x80', 'b' }; + + ASSERT_NULL(ellipsize_mem(s, 2, 1, 50)); +} + static void test_ellipsize_one(const char *p) { _cleanup_free_ char *t = NULL; t = ellipsize(p, columns(), 70);