Commit 309f963f for tesseract

commit 309f963f50bd3e6732654a114550d7d32da6c278
Author: Som Samantray <92726151+SomSamantray@users.noreply.github.com>
Date:   Wed Sep 2 13:28:48 2026 +0530

    fix: prevent out-of-bounds read in UNICHAR::UTF8ToUTF32 (#4609)

    * test: add regression test for truncated UTF-8 in UTF8ToUTF32
    * test: explain truncated UTF-8 terminators (AI-assisted)

    Adds UnicharTest.TruncatedUtf8 covering 2/3/4-byte truncated prefixes,
    a mid-string truncated prefix, and an illegal leading continuation byte.
    The test triggers the out-of-bounds read in UNICHAR::UTF8ToUTF32 on
    unfixed code (issue #4495).

    utf8_step() reports the full multibyte width from the leading byte
    alone, so a truncated prefix (e.g. "\xE8\0") made the iterator read
    one byte past the end of the string. Clamp the per-character step to
    the remaining bytes and return an empty vector for truncated input.

    Fixes #4495.

    Reviewed-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/ccutil/unichar.cpp b/src/ccutil/unichar.cpp
index ae6d6133..f7a98dbc 100644
--- a/src/ccutil/unichar.cpp
+++ b/src/ccutil/unichar.cpp
@@ -223,12 +223,16 @@ std::vector<char32> UNICHAR::UTF8ToUTF32(const char *utf8_str) {
   unicodes.reserve(utf8_length);
   const_iterator end_it(end(utf8_str, utf8_length));
   for (const_iterator it(begin(utf8_str, utf8_length)); it != end_it; ++it) {
-    if (it.is_legal()) {
-      unicodes.push_back(*it);
-    } else {
+    // utf8_step() reports the width from the leading byte alone; reject a
+    // truncated trailing sequence rather than let the iterator run past the
+    // end of the string (issue #4495).
+    const int remaining = end_it.utf8_data() - it.utf8_data();
+    const int step = utf8_step(it.utf8_data());
+    if (step <= 0 || step > remaining) {
       unicodes.clear();
       return unicodes;
     }
+    unicodes.push_back(*it);
   }
   return unicodes;
 }
diff --git a/unittest/unichar_test.cc b/unittest/unichar_test.cc
index e03dad17..028be7dc 100644
--- a/unittest/unichar_test.cc
+++ b/unittest/unichar_test.cc
@@ -40,4 +40,23 @@ TEST(UnicharTest, InvalidText) {
   EXPECT_TRUE(utf8.empty());
 }

+TEST(UnicharTest, TruncatedUtf8) {
+  // This test verifies that UTF8ToUTF32 does not read past the end of a
+  // string that ends with a truncated multibyte prefix (issue #4495).
+  // A truncated multibyte prefix is invalid UTF-8, so the conversion
+  // must return an empty vector instead of reading past the NUL.
+  // Keep the explicit NULs to make the truncation boundary visible in each
+  // fixture; without them, the literal terminator is implicit.
+  const char *kTruncated2 = "\xC2\0";
+  const char *kTruncated3 = "\xE8\0";
+  const char *kTruncated4 = "\xF0\0";
+  const char *kTruncatedMid = "ab\xE8\0";
+  const char *kIllegalLeading = "\x80\0";
+  EXPECT_TRUE(UNICHAR::UTF8ToUTF32(kTruncated2).empty());
+  EXPECT_TRUE(UNICHAR::UTF8ToUTF32(kTruncated3).empty());
+  EXPECT_TRUE(UNICHAR::UTF8ToUTF32(kTruncated4).empty());
+  EXPECT_TRUE(UNICHAR::UTF8ToUTF32(kTruncatedMid).empty());
+  EXPECT_TRUE(UNICHAR::UTF8ToUTF32(kIllegalLeading).empty());
+}
+
 } // namespace tesseract