]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
string-util: add upper bound to ellipsize_mem UTF-8 walks
authorLuca Boccassi <luca.boccassi@gmail.com>
Thu, 9 Jul 2026 10:15:32 +0000 (11:15 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Tue, 14 Jul 2026 15:43:28 +0000 (16:43 +0100)
Validate UTF-8 characters against the caller-provided byte slice.
This stops truncated sequences from borrowing bytes past old_length.

Follow-up for e69cc9eb36fd6e76710b4d5f4bb7013980fb5174

src/basic/string-util.c
src/test/test-ellipsize.c

index 1dc90d538ad01f5f3e259ebdddb264832de9a30d..047a58ade571253038ceb75441704224b47e01a9 100644 (file)
@@ -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;
index 2efb123ac2b7318813036235fc6addaf89a94b1b..3d47cfebe2610889eb5ca72901959e9326532ccc 100644 (file)
@@ -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);