tesseract: patch CVE-2026-88054

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

(cherry picked from commit 4c00053135)

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:17 -07:00
1 parent 4c928862f5
commit b9c3d34642
2 files changed
+275

No files matched your search

@@ -0,0 +1,274 @@
From 83bbac0975f52a5171776fddcc800a2aaa5206a3 Mon Sep 17 00:00:00 2001
From: Stefan Weil <sw@weilnetz.de>
Date: Fri, 21 Aug 2026 14:38:55 +0200
Subject: [PATCH] Reject empty network stacks in LSTM .traineddata
deserialization
Plumbing::DeSerialize read the network stack size from the untrusted
TESSDATA_LSTM component and only rejected size > 10000. A crafted
.traineddata with a top-level Series/Parallel/Reversed layer with an
empty stack survived load; LSTMRecognizer initialization then called
network_->CacheXScaleFactor(network_->XScaleFactor()), which
dereferences stack_[0] on the empty vector:
- Series: Series::CacheXScaleFactor (series.cpp)
- Parallel/Reversed: inherited Plumbing::XScaleFactor
The virtual call through the wild pointer crashed the process at
initialization (deterministic denial of service).
Key changes:
- plumbing.cpp: reject size == 0 for all plumbing types, and
size < 2 for NT_SERIES (Series::Forward requires two or more
networks and always aborts on one).
- tessedit.cpp: fail the language load gracefully when the LSTM model
cannot be loaded, instead of aborting via ASSERT_HOST, so
TessBaseAPI::Init returns -1 on corrupt traineddata.
- unittest: add plumbing_test, which builds a minimal traineddata
with empty/undersized LSTM plumbing stacks and expects a graceful
init failure. On unpatched code the tests die on the original
SEGV in Series::CacheXScaleFactor / Plumbing::XScaleFactor.
Reported-by: Zhixi "Jace" Sun <g.mygenie@gmail.com>
Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
(cherry picked from commit 552771236b0d80cbdb0c7dd856120fa21a4672e5)
CVE: CVE-2026-88054
Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/552771236b0d80cbdb0c7dd856120fa21a4672e5]
Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
Makefile.am | 5 ++
src/ccmain/tessedit.cpp | 7 +-
src/lstm/plumbing.cpp | 6 ++
unittest/plumbing_test.cc | 165 ++++++++++++++++++++++++++++++++++++++
4 files changed, 182 insertions(+), 1 deletion(-)
create mode 100644 unittest/plumbing_test.cc
diff --git a/Makefile.am b/Makefile.am
index c67f0f63..a6ee0931 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1198,6 +1198,7 @@ if ENABLE_TRAINING
check_PROGRAMS += pango_font_info_test
endif # ENABLE_TRAINING
check_PROGRAMS += paragraphs_test
+check_PROGRAMS += plumbing_test
if !DISABLED_LEGACY_ENGINE
check_PROGRAMS += params_model_test
endif # !DISABLED_LEGACY_ENGINE
@@ -1427,6 +1428,10 @@ paragraphs_test_SOURCES = unittest/paragraphs_test.cc
paragraphs_test_CPPFLAGS = $(unittest_CPPFLAGS)
paragraphs_test_LDADD = $(TESS_LIBS)
+plumbing_test_SOURCES = unittest/plumbing_test.cc
+plumbing_test_CPPFLAGS = $(unittest_CPPFLAGS)
+plumbing_test_LDADD = $(TESS_LIBS)
+
if !DISABLED_LEGACY_ENGINE
params_model_test_SOURCES = unittest/params_model_test.cc
params_model_test_CPPFLAGS = $(unittest_CPPFLAGS)
diff --git a/src/ccmain/tessedit.cpp b/src/ccmain/tessedit.cpp
index 7ec89bd9..722ed09b 100644
--- a/src/ccmain/tessedit.cpp
+++ b/src/ccmain/tessedit.cpp
@@ -169,7 +169,12 @@ bool Tesseract::init_tesseract_lang_data(const std::string &language, OcrEngineM
#endif // ndef DISABLED_LEGACY_ENGINE
if (mgr->IsComponentAvailable(TESSDATA_LSTM)) {
lstm_recognizer_ = new LSTMRecognizer(language_data_path_prefix.c_str());
- ASSERT_HOST(lstm_recognizer_->Load(this->params(), lstm_use_matrix ? language : "", mgr));
+ if (!lstm_recognizer_->Load(this->params(), lstm_use_matrix ? language : "", mgr)) {
+ delete lstm_recognizer_;
+ lstm_recognizer_ = nullptr;
+ tprintf("Error: Failed to load the LSTM model from %s\n", tessdata_path.c_str());
+ return false;
+ }
} else {
#ifdef DISABLED_LEGACY_ENGINE
// The legacy engine is compiled out, so we cannot fall back to it.
diff --git a/src/lstm/plumbing.cpp b/src/lstm/plumbing.cpp
index f0133148..15cf3d62 100644
--- a/src/lstm/plumbing.cpp
+++ b/src/lstm/plumbing.cpp
@@ -226,6 +226,12 @@ bool Plumbing::DeSerialize(TFile *fp) {
if (size > 10000) {
return false;
}
+ // Reject empty stacks: XScaleFactor, CacheXScaleFactor and other methods
+ // unconditionally dereference stack_[0] during network initialization.
+ // A Series needs at least two networks (see Series::Forward).
+ if (size == 0 || (type() == NT_SERIES && size == 1)) {
+ return false;
+ }
for (uint32_t i = 0; i < size; ++i) {
Network *network = CreateFromFile(fp);
if (network == nullptr) {
diff --git a/unittest/plumbing_test.cc b/unittest/plumbing_test.cc
new file mode 100644
index 00000000..27486f86
--- /dev/null
+++ b/unittest/plumbing_test.cc
@@ -0,0 +1,165 @@
+///////////////////////////////////////////////////////////////////////
+// File: plumbing_test.cc
+// Description: Tests that a corrupt TESSDATA_LSTM component in a
+// .traineddata file is rejected without crashing. A
+// plumbing layer (Series/Parallel/Reversed) with an
+// empty or undersized network stack would make
+// XScaleFactor/CacheXScaleFactor dereference stack_[0]
+// during 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 <tesseract/baseapi.h>
+
+#include "network.h" // for NetworkType
+#include "tessdatamanager.h" // for TessdataManager, TESSDATA_LSTM
+
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Minimal unicharset with the special codes (space, Joined, Broken) that
+// UNICHARSET::load_from_file expects, as embedded in the TESSDATA_LSTM
+// component by LSTMRecognizer::Serialize.
+const char kMinUnicharset[] =
+ "3\n"
+ "NULL 0 NULL 0\n"
+ "Joined 7 0,69,188,255,486,1218,0,30,486,1188 Latin 26 0 98 Joined\n"
+ "|Broken|0|1 f 0,69,186,255,892,2138,0,80,892,2058 Common 84 10 84 |Broken|0|1\n";
+
+// Appends raw little-endian values to a byte buffer.
+class ByteWriter {
+public:
+ void PutU8(uint32_t v) { data_.push_back(static_cast<char>(v & 0xFF)); }
+ void PutU32(uint32_t v) {
+ for (int i = 0; i < 4; ++i) {
+ data_.push_back(static_cast<char>((v >> (8 * i)) & 0xFF));
+ }
+ }
+ void PutS32(int32_t v) { PutU32(static_cast<uint32_t>(v)); }
+ void PutString(const char *s) {
+ PutU32(static_cast<uint32_t>(std::strlen(s)));
+ data_.insert(data_.end(), s, s + std::strlen(s));
+ }
+ void PutRaw(const char *s) { data_.insert(data_.end(), s, s + std::strlen(s)); }
+ const std::vector<char> &data() const { return data_; }
+
+private:
+ std::vector<char> data_;
+};
+
+// Serialized network header as written by Network::Serialize:
+// int8 type, int8 training, int8 needs_to_backprop, int32 network_flags,
+// int32 ni, int32 no, int32 num_weights, string name.
+void AppendNetworkHeader(ByteWriter *w, NetworkType type) {
+ w->PutU8(static_cast<uint32_t>(type));
+ w->PutU8(0); // training: TS_DISABLED
+ w->PutU8(0); // needs_to_backprop
+ w->PutU32(0); // network_flags
+ w->PutU32(0); // ni
+ w->PutU32(0); // no
+ w->PutU32(0); // num_weights
+ w->PutString(""); // name
+}
+
+// A minimal valid child network (NT_INPUT with a 1x1x1x1 shape).
+void AppendInputChild(ByteWriter *w) {
+ AppendNetworkHeader(w, NT_INPUT);
+ w->PutS32(1); // batch
+ w->PutS32(1); // height
+ w->PutS32(1); // width
+ w->PutS32(1); // depth
+ w->PutS32(0); // loss type
+}
+
+// Builds a TESSDATA_LSTM component whose top-level network is a plumbing
+// layer of the given type with the given (corrupt) stack size, followed by
+// the remaining fields of LSTMRecognizer::DeSerialize.
+std::vector<char> MakeLstmComponent(NetworkType type, uint32_t stack_size) {
+ ByteWriter w;
+ AppendNetworkHeader(&w, type);
+ w.PutU32(stack_size); // Plumbing::DeSerialize reads this as uint32
+ for (uint32_t i = 0; i < stack_size; ++i) {
+ AppendInputChild(&w);
+ }
+ w.PutRaw(kMinUnicharset); // unicharset (raw text, no recoder/unicharset components)
+ w.PutString(""); // network_str_
+ w.PutS32(0); // training_flags_
+ w.PutS32(0); // training_iteration_
+ w.PutS32(0); // sample_iteration_
+ w.PutS32(0); // null_char_
+ w.PutU32(0); // adam_beta_ (float 0.0)
+ w.PutU32(0); // learning_rate_ (float 0.0)
+ w.PutU32(0); // momentum_ (float 0.0)
+ return w.data();
+}
+
+// Writes a traineddata file with the given (corrupt) LSTM component to
+// dir/eng.traineddata.
+bool WriteCorruptTraineddata(const std::string &dir, const std::vector<char> &lstm) {
+ TessdataManager mgr;
+ mgr.OverwriteEntry(TESSDATA_LSTM, lstm.data(), static_cast<int>(lstm.size()));
+ return mgr.SaveFile((dir + "/eng.traineddata").c_str(), nullptr);
+}
+
+class PlumbingTest : public testing::Test {
+protected:
+ void SetUp() override {
+ tmpl_ = "/tmp/tess_plumbing_test_XXXXXX";
+ char *dir = mkdtemp(tmpl_.data());
+ ASSERT_NE(dir, nullptr);
+ dir_ = dir;
+ }
+ void TearDown() override {
+ std::remove((dir_ + "/eng.traineddata").c_str());
+ rmdir(dir_.c_str());
+ }
+ // Expects the LSTM engine to reject the corrupted traineddata
+ // gracefully (init failure) instead of crashing.
+ void ExpectInitFails(const std::vector<char> &lstm) {
+ ASSERT_TRUE(WriteCorruptTraineddata(dir_, lstm));
+ tesseract::TessBaseAPI api;
+ EXPECT_EQ(api.Init(dir_.c_str(), "eng", tesseract::OEM_LSTM_ONLY), -1);
+ }
+ std::string dir_;
+ std::string tmpl_;
+};
+
+// Empty NT_SERIES stack: Series::CacheXScaleFactor would dereference
+// stack_[0] on the empty vector during initialization.
+TEST_F(PlumbingTest, RejectsEmptySeriesStack) {
+ ExpectInitFails(MakeLstmComponent(NT_SERIES, 0));
+}
+
+// Empty NT_PARALLEL stack: Plumbing::XScaleFactor would dereference
+// stack_[0] on the empty vector during initialization.
+TEST_F(PlumbingTest, RejectsEmptyParallelStack) {
+ ExpectInitFails(MakeLstmComponent(NT_PARALLEL, 0));
+}
+
+// Empty NT_XREVERSED stack: same crash as the parallel case.
+TEST_F(PlumbingTest, RejectsEmptyReversedStack) {
+ ExpectInitFails(MakeLstmComponent(NT_XREVERSED, 0));
+}
+
+// A Series with a single network: Series::Forward requires at least two
+// networks, so such a model can never work.
+TEST_F(PlumbingTest, RejectsSingleNetworkSeries) {
+ ExpectInitFails(MakeLstmComponent(NT_SERIES, 1));
+}
+
+} // namespace
+} // namespace tesseract
@@ -13,6 +13,7 @@ SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag
file://CVE-2026-88050.patch \
file://CVE-2026-88047.patch \
file://CVE-2026-88051.patch \
file://CVE-2026-88054.patch \
"
DEPENDS = "leptonica"