Commit 8caea666 for tesseract

commit 8caea666a444a21e90b76e5fafc890fc0b18ec33
Author: Stefan Weil <sw@weilnetz.de>
Date:   Tue Aug 25 21:56:12 2026 +0200

    Skip truncated UTF-8 in PangoFontInfo character loops

    CoversUTF8Text, DropUncoveredChars and GetSpacingProperties iterated
    with UNICHAR::const_iterator over byte ranges that may end in a
    truncated multibyte sequence. The iterator's *it and is_legal read the
    full width from the leading byte, so a truncated trailing prefix (for
    example "ab\xE8") read past the end of the string and ++it jumped
    past the end iterator: an out-of-bounds read that only stopped at a
    crash.

    The loops now step the bytes manually: an illegal leading byte keeps
    the iterator's space fallback, and a sequence truncated at the end of
    the string is skipped one byte at a time (dropped and counted by
    DropUncoveredChars, skipped by the other two) instead of read past the
    end. Valid UTF-8 input is handled exactly as before.

    Key changes:
    - pango_font_info.cpp: rewrite the three character loops with explicit
      width checks against the remaining bytes.
    - unittest: add HandlesTruncatedUtf8. On unpatched code the test dies
      on an ASan stack-buffer-overflow read in UNICHAR::utf8_step via
      is_legal() in DropUncoveredChars.

    Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/training/pango/pango_font_info.cpp b/src/training/pango/pango_font_info.cpp
index 42ae3921..380f4255 100644
--- a/src/training/pango/pango_font_info.cpp
+++ b/src/training/pango/pango_font_info.cpp
@@ -222,23 +222,31 @@ bool PangoFontInfo::CoversUTF8Text(const char *utf8_text, int byte_length) const
     return false;
   }
   PangoCoverage *coverage = pango_font_get_coverage(font, nullptr);
-  for (UNICHAR::const_iterator it = UNICHAR::begin(utf8_text, byte_length);
-       it != UNICHAR::end(utf8_text, byte_length); ++it) {
-    if (IsWhitespace(*it) || pango_is_zero_width(*it)) {
-      continue;
-    }
-    if (pango_coverage_get(coverage, *it) != PANGO_COVERAGE_EXACT) {
-      char tmp[5];
-      int len = it.get_utf8(tmp);
-      tmp[len] = '\0';
-      tlog(2, "'%s' (U+%x) not covered by font\n", tmp, *it);
+  const char *const text_end = utf8_text + byte_length;
+  for (const char *p = utf8_text; p < text_end;) {
+    const int step = UNICHAR::utf8_step(p);
+    if (step > 0 && step <= text_end - p) {
+      const int unicode = UNICHAR(p, step).first_uni();
+      if (!IsWhitespace(unicode) && !pango_is_zero_width(unicode) &&
+          pango_coverage_get(coverage, unicode) != PANGO_COVERAGE_EXACT) {
+        char tmp[5];
+        memcpy(tmp, p, step);
+        tmp[step] = '\0';
+        tlog(2, "'%s' (U+%x) not covered by font\n", tmp, unicode);
 #if PANGO_VERSION_CHECK(1, 50, 4)
-      g_object_unref(coverage);
+        g_object_unref(coverage);
 #else
-      pango_coverage_unref(coverage);
+        pango_coverage_unref(coverage);
 #endif
-      g_object_unref(font);
-      return false;
+        g_object_unref(font);
+        return false;
+      }
+      p += step;
+    } else {
+      // An illegal byte or a multibyte sequence truncated at the end of the
+      // string is not a character to check coverage for. Skipping it one
+      // byte at a time also avoids reading past the end of the string.
+      ++p;
     }
   }
 #if PANGO_VERSION_CHECK(1, 50, 4)
@@ -286,19 +294,22 @@ int PangoFontInfo::DropUncoveredChars(std::string *utf8_text) const {
   // will repeatedly copy one covered UTF8 character from one to the other, and
   // at the end resize the string to the right length.
   char *out = const_cast<char *>(utf8_text->c_str());
-  const UNICHAR::const_iterator it_begin = UNICHAR::begin(utf8_text->c_str(), utf8_text->length());
-  const UNICHAR::const_iterator it_end = UNICHAR::end(utf8_text->c_str(), utf8_text->length());
-  for (UNICHAR::const_iterator it = it_begin; it != it_end;) {
-    // Skip bad utf-8.
-    if (!it.is_legal()) {
-      ++it; // One suitable error message will still be issued.
+  const char *const in_end = utf8_text->c_str() + utf8_text->length();
+  for (const char *p = utf8_text->c_str(); p < in_end;) {
+    const int step = UNICHAR::utf8_step(p);
+    if (step <= 0) {
+      // Skip bad utf-8.
+      ++p;
       continue;
     }
-    int unicode = *it;
-    int utf8_len = it.utf8_len();
-    const char *utf8_char = it.utf8_data();
-    // Move it forward before the data gets modified.
-    ++it;
+    if (step > in_end - p) {
+      // A multibyte sequence truncated at the end of the string: drop the
+      // stray bytes instead of reading past the NUL terminator.
+      ++num_dropped_chars;
+      ++p;
+      continue;
+    }
+    const int unicode = UNICHAR(p, step).first_uni();
     if (!IsWhitespace(unicode) && !pango_is_zero_width(unicode) &&
         pango_coverage_get(coverage, unicode) != PANGO_COVERAGE_EXACT) {
       if (TLOG_IS_ON(2)) {
@@ -308,10 +319,11 @@ int PangoFontInfo::DropUncoveredChars(std::string *utf8_text) const {
         delete[] str;
       }
       ++num_dropped_chars;
-      continue;
+    } else {
+      my_strnmove(out, p, step);
+      out += step;
     }
-    my_strnmove(out, utf8_char, utf8_len);
-    out += utf8_len;
+    p += step;
   }
 #if PANGO_VERSION_CHECK(1, 50, 4)
   g_object_unref(coverage);
@@ -336,10 +348,26 @@ bool PangoFontInfo::GetSpacingProperties(const std::string &utf8_char, int *x_be
   // Handle multi-unicode strings by reporting the left-most position of the
   // x-bearing, and right-most position of the x-advance if the string were to
   // be rendered.
-  const UNICHAR::const_iterator it_begin = UNICHAR::begin(utf8_char.c_str(), utf8_char.length());
-  const UNICHAR::const_iterator it_end = UNICHAR::end(utf8_char.c_str(), utf8_char.length());
-  for (UNICHAR::const_iterator it = it_begin; it != it_end; ++it) {
-    PangoGlyph glyph_index = get_glyph(font, *it);
+  bool first_char = true;
+  const char *p = utf8_char.c_str();
+  const char *const p_end = p + utf8_char.length();
+  for (; p < p_end;) {
+    const int step = UNICHAR::utf8_step(p);
+    int unicode;
+    if (step <= 0) {
+      // The iterator maps an illegal leading byte to a space.
+      unicode = ' ';
+      ++p;
+    } else if (step > p_end - p) {
+      // A multibyte sequence truncated at the end of the string: skip the
+      // stray bytes instead of reading past the NUL terminator.
+      ++p;
+      continue;
+    } else {
+      unicode = UNICHAR(p, step).first_uni();
+      p += step;
+    }
+    PangoGlyph glyph_index = get_glyph(font, unicode);
     if (!glyph_index) {
       // Glyph for given unicode character doesn't exist in font.
       g_object_unref(font);
@@ -352,9 +380,10 @@ bool PangoFontInfo::GetSpacingProperties(const std::string &utf8_char, int *x_be
     pango_extents_to_pixels(&logical_rect, nullptr);

     int bearing = total_advance + PANGO_LBEARING(ink_rect);
-    if (it == it_begin || bearing < min_bearing) {
+    if (first_char || bearing < min_bearing) {
       min_bearing = bearing;
     }
+    first_char = false;
     total_advance += PANGO_RBEARING(logical_rect);
   }
   *x_bearing = min_bearing;
diff --git a/unittest/pango_font_info_test.cc b/unittest/pango_font_info_test.cc
index 27938843..e9d98e85 100644
--- a/unittest/pango_font_info_test.cc
+++ b/unittest/pango_font_info_test.cc
@@ -171,6 +171,20 @@ TEST_F(PangoFontInfoTest, CanDropUncoveredChars) {
   }
 }

+TEST_F(PangoFontInfoTest, HandlesTruncatedUtf8) {
+  font_info_.ParseFontDescriptionName("Verdana 12");
+  // A multibyte sequence truncated at the end of the string must not be
+  // read past the end of the string (or loop forever).
+  std::string word = std::string("ab") + "\xE8";
+  EXPECT_EQ(1, font_info_.DropUncoveredChars(&word));
+  EXPECT_STREQ("ab", word.c_str());
+  const std::string covers = std::string("ab") + "\xF0";
+  EXPECT_TRUE(font_info_.CoversUTF8Text(covers.c_str(), covers.length()));
+  int x_bearing, x_advance;
+  EXPECT_TRUE(font_info_.GetSpacingProperties(covers, &x_bearing, &x_advance));
+  EXPECT_GT(x_advance, 0);
+}
+
 // ------------------------ FontUtils ------------------------------------

 class FontUtilsTest : public ::testing::Test {