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 {