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