tesseract: patch CVE-2026-88047

Details:
https://nvd.nist.gov/vuln/detail/cve-2026-88047

(cherry picked from commit b8fa7aac91)

Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
Signed-off-by: Khem Raj <khem.raj@oss.qualcomm.com>
This commit is contained in:
Ankur Tyagi authored and Khem Raj committed 2026-10-12 21:57:16 -07:00
1 parent cc2b58f135
commit 2ba3ed5dc8
2 files changed
+226

No files matched your search

@@ -0,0 +1,225 @@
From e974684acbe3aeb6bc6192b06a048537c345b713 Mon Sep 17 00:00:00 2001
From: Stefan Weil <sw@weilnetz.de>
Date: Mon, 24 Aug 2026 13:39:22 +0200
Subject: [PATCH] Limit unichar extraction in ReadNormProtos to the buffer size
Classify::ReadNormProtos parsed each normproto line with
`stream >> unichar >> NumProtos` into a char unichar[2 * UNICHAR_LEN + 1]
stack buffer, but char* extraction from an istream has no length limit
(the stream width was never set). A crafted TESSDATA_NORMPROTO component
in a .traineddata file whose first proto-line token exceeds 60
characters (the 100-byte line buffer allows up to 99) overflows the
stack buffer during legacy engine initialization (CWE-121).
Toolchain note: Apple's libc++ provides a C++20 array overload of
operator>>(basic_istream&, char(&)[N]) that implicitly bounds the
extraction to the array size, so builds against that standard library
are incidentally protected. Standard libraries without that overload
(e.g. libstdc++) still take the unbounded char* overload, so the
explicit width limit below makes the behavior defined on all
toolchains.
Key changes:
- normmatch.cpp: read the unichar token with
std::setw(2 * UNICHAR_LEN + 1); char* extraction takes at most
width - 1 characters, which fits the buffer exactly. Overlong
tokens are truncated and the line is rejected like any other
unparseable line.
- unittest: add normproto_test covering a 99-character token (the
maximum a 100-byte line can hold), a token of exactly 2 *
UNICHAR_LEN characters, and a well-formed component. A minimal
reproduction of the unbounded extraction crashes an ASan build
with a stack-buffer-overflow.
Reported-by: Tristan Madani <tristan@talencesecurity.com>
Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
(cherry picked from commit 1bda5079b1c8a7e25f523486837426903d29ce84)
CVE: CVE-2026-88047
Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/1bda5079b1c8a7e25f523486837426903d29ce84]
Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
Makefile.am | 5 ++
src/classify/normmatch.cpp | 6 +-
unittest/CMakeLists.txt | 1 +
unittest/normproto_test.cc | 111 +++++++++++++++++++++++++++++++++++++
4 files changed, 122 insertions(+), 1 deletion(-)
create mode 100644 unittest/normproto_test.cc
diff --git a/Makefile.am b/Makefile.am
index 63ca6686..cc748455 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1184,6 +1184,7 @@ check_PROGRAMS += mastertrainer_test
endif # !DISABLED_LEGACY_ENGINE
check_PROGRAMS += matrix_test
check_PROGRAMS += networkio_test
+check_PROGRAMS += normproto_test
if ENABLE_TRAINING
check_PROGRAMS += normstrngs_test
endif # ENABLE_TRAINING
@@ -1388,6 +1389,10 @@ networkio_test_SOURCES = unittest/networkio_test.cc
networkio_test_CPPFLAGS = $(unittest_CPPFLAGS)
networkio_test_LDADD = $(TESS_LIBS)
+normproto_test_SOURCES = unittest/normproto_test.cc
+normproto_test_CPPFLAGS = $(unittest_CPPFLAGS)
+normproto_test_LDADD = $(TESS_LIBS)
+
normstrngs_test_SOURCES = unittest/normstrngs_test.cc
normstrngs_test_CPPFLAGS = $(unittest_CPPFLAGS)
normstrngs_test_LDADD = $(TRAINING_LIBS) $(ICU_I18N_LIBS) $(ICU_UC_LIBS)
diff --git a/src/classify/normmatch.cpp b/src/classify/normmatch.cpp
index d5bd7e6a..1ce7c928 100644
--- a/src/classify/normmatch.cpp
+++ b/src/classify/normmatch.cpp
@@ -28,6 +28,7 @@
#include <cmath>
#include <cstdio>
+#include <iomanip> // for std::setw
#include <sstream> // for std::istringstream
namespace tesseract {
@@ -190,7 +191,10 @@ NORM_PROTOS *Classify::ReadNormProtos(TFile *fp) {
while (fp->FGets(line, kMaxLineSize) != nullptr) {
std::istringstream stream(line);
stream.imbue(std::locale::classic());
- stream >> unichar >> NumProtos;
+ // unichar holds at most 2 * UNICHAR_LEN characters; the width limit
+ // (width - 1 characters for char* extraction) keeps the extraction
+ // from overflowing the buffer on overlong lines.
+ stream >> std::setw(2 * UNICHAR_LEN + 1) >> unichar >> NumProtos;
if (stream.fail()) {
continue;
}
diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
index a660c9b1..4c677a60 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -59,6 +59,7 @@ set(LEGACY_TESTS
indexmapbidi_test.cc
intfeaturemap_test.cc
mastertrainer_test.cc
+ normproto_test.cc
osd_test.cc
params_model_test.cc
shapetable_test.cc
diff --git a/unittest/normproto_test.cc b/unittest/normproto_test.cc
new file mode 100644
index 00000000..574b2f3d
--- /dev/null
+++ b/unittest/normproto_test.cc
@@ -0,0 +1,111 @@
+///////////////////////////////////////////////////////////////////////
+// File: normproto_test.cc
+// Description: Tests that Classify::ReadNormProtos handles a normproto
+// line whose first (unichar) token exceeds the
+// unichar[2 * UNICHAR_LEN + 1] stack buffer. The
+// istream extraction has no intrinsic length limit, so a
+// crafted NORMPROTO component in a .traineddata file
+// could overflow the stack buffer during legacy engine
+// initialization.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+///////////////////////////////////////////////////////////////////////
+
+#include "include_gunit.h"
+
+#include "classify.h"
+#include "serialis.h" // for TFile
+
+#include <cstdio>
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Minimal unicharset (space and 'a').
+const char kMinUnicharset[] =
+ "2\n"
+ "NULL 1 0,255,0,255,0,0,0,0,0,0 Latin 2 0 2\n"
+ "a 1 0,255,0,255,0,0,0,0,0,0 Latin 2 0 2\n";
+
+// Builds a normproto component: a sample-size line (5), five parameter
+// description lines, then the given raw proto lines.
+std::vector<char> MakeNormproto(const std::string &lines) {
+ std::string data = "5\n";
+ for (int i = 0; i < 5; ++i) {
+ data += "e e 0 1\n";
+ }
+ data += lines;
+ return std::vector<char>(data.begin(), data.end());
+}
+
+class NormprotoTest : public testing::Test {
+protected:
+ void SetUp() override {
+ tmpl_ = "/tmp/tess_normproto_test_XXXXXX";
+ char *dir = mkdtemp(tmpl_.data());
+ ASSERT_NE(dir, nullptr);
+ dir_ = dir;
+ std::string uc_path = dir_ + "/eng.unicharset";
+ FILE *f = fopen(uc_path.c_str(), "w");
+ ASSERT_NE(f, nullptr);
+ ASSERT_EQ(fwrite(kMinUnicharset, 1, sizeof(kMinUnicharset) - 1, f),
+ sizeof(kMinUnicharset) - 1);
+ fclose(f);
+ // Load the minimal unicharset into the classifier's inherited
+ // unicharset member.
+ ASSERT_TRUE(classifier_.unicharset.load_from_file(uc_path.c_str()));
+ }
+ void TearDown() override {
+ std::remove((dir_ + "/eng.unicharset").c_str());
+ rmdir(dir_.c_str());
+ }
+ std::string dir_;
+ std::string tmpl_;
+ Classify classifier_;
+};
+
+// A 99-character first token (the maximum FGets can return) overflows
+// unichar[2 * UNICHAR_LEN + 1] on unpatched code; the width-limited
+// extraction must reject the line instead.
+TEST_F(NormprotoTest, ToleratesOverlongUnicharToken) {
+ std::vector<char> bytes = MakeNormproto(std::string(99, 'A') + "\n");
+ TFile fp;
+ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+ classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
+ ASSERT_NE(classifier_.NormProtos, nullptr);
+ classifier_.FreeNormProtos();
+ EXPECT_EQ(classifier_.NormProtos, nullptr);
+}
+
+// A token of exactly 2 * UNICHAR_LEN characters is the maximum legitimate
+// size; it must not be truncated or rejected by the width limit.
+TEST_F(NormprotoTest, ToleratesMaxLenUnicharToken) {
+ std::vector<char> bytes = MakeNormproto(std::string(2 * UNICHAR_LEN, 'A') + " 0\n");
+ TFile fp;
+ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+ classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
+ ASSERT_NE(classifier_.NormProtos, nullptr);
+ classifier_.FreeNormProtos();
+ EXPECT_EQ(classifier_.NormProtos, nullptr);
+}
+
+// A well-formed normproto component must still parse.
+TEST_F(NormprotoTest, ReadsValidNormprotos) {
+ std::vector<char> bytes = MakeNormproto("a 0\n");
+ TFile fp;
+ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+ classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
+ ASSERT_NE(classifier_.NormProtos, nullptr);
+ classifier_.FreeNormProtos();
+ EXPECT_EQ(classifier_.NormProtos, nullptr);
+}
+
+} // namespace
+} // namespace tesseract
@@ -11,6 +11,7 @@ SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag
file://CVE-2026-88048.patch \
file://CVE-2026-88049.patch \
file://CVE-2026-88050.patch \
file://CVE-2026-88047.patch \
"
DEPENDS = "leptonica"