public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Sandro Mani <manisandro@gmail.com>
To: git-commits@fedoraproject.org
Subject: [rpms/tesseract] f43: Backport fixes for CVE-2026-{88047-88054}
Date: Mon, 21 Sep 2026 08:47:10 GMT [thread overview]
Message-ID: <178998043063.1.11241291053047815428.rpms-tesseract-8f15530a402e@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/tesseract
Branch : f43
Commit : 8f15530a402eca6406382064c9279d1b2f668a78
Author : Sandro Mani <manisandro@gmail.com>
Date : 2026-09-21T10:46:25+02:00
Stats : +2034/-5 in 10 file(s)
URL : https://src.fedoraproject.org/rpms/tesseract/c/8f15530a402eca6406382064c9279d1b2f668a78?branch=f43
Log:
Backport fixes for CVE-2026-{88047-88054}
---
diff --git a/103dc134eb36411ddc6833ec20aa2c76795bd0ff.patch b/103dc134eb36411ddc6833ec20aa2c76795bd0ff.patch
new file mode 100644
index 0000000..6a1b2ea
--- /dev/null
+++ b/103dc134eb36411ddc6833ec20aa2c76795bd0ff.patch
@@ -0,0 +1,179 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.192448773 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.197903363 +0200
+@@ -1158,6 +1158,7 @@ if !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += equationdetect_test
+ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += fileio_test
++check_PROGRAMS += fullyconnected_test
+ check_PROGRAMS += heap_test
+ check_PROGRAMS += imagedata_test
+ if !DISABLED_LEGACY_ENGINE
+@@ -1290,6 +1291,10 @@ fileio_test_SOURCES = unittest/fileio_te
+ fileio_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ fileio_test_LDADD = $(TRAINING_LIBS)
+
++fullyconnected_test_SOURCES = unittest/fullyconnected_test.cc
++fullyconnected_test_CPPFLAGS = $(unittest_CPPFLAGS)
++fullyconnected_test_LDADD = $(TESS_LIBS)
++
+ heap_test_SOURCES = unittest/heap_test.cc
+ heap_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ heap_test_LDADD = $(TESS_LIBS)
+diff -rupN --no-dereference tesseract-5.5.3/src/lstm/fullyconnected.cpp tesseract-5.5.3-new/src/lstm/fullyconnected.cpp
+--- tesseract-5.5.3/src/lstm/fullyconnected.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/lstm/fullyconnected.cpp 2026-09-21 10:28:10.198512372 +0200
+@@ -121,7 +121,16 @@ bool FullyConnected::Serialize(TFile *fp
+
+ // Reads from the given file. Returns false in case of error.
+ bool FullyConnected::DeSerialize(TFile *fp) {
+- return weights_.DeSerialize(IsTraining(), fp);
++ if (!weights_.DeSerialize(IsTraining(), fp)) {
++ return false;
++ }
++ // The weight matrix must match the declared sizes (the second dimension
++ // includes the bias column); otherwise Forward would read or write
++ // outside the scratch buffers sized from ni_ and no_.
++ if (weights_.Dim1() != no_ || weights_.Dim2() != ni_ + 1) {
++ return false;
++ }
++ return true;
+ }
+
+ // Runs forward propagation of activations on the input line.
+diff -rupN --no-dereference tesseract-5.5.3/src/lstm/weightmatrix.h tesseract-5.5.3-new/src/lstm/weightmatrix.h
+--- tesseract-5.5.3/src/lstm/weightmatrix.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/lstm/weightmatrix.h 2026-09-21 10:28:10.198819056 +0200
+@@ -107,6 +107,13 @@ public:
+ int NumOutputs() const {
+ return int_mode_ ? wi_.dim1() : wf_.dim1();
+ }
++ // The dimensions of the active weight matrix (wi_ in int mode, else wf_).
++ int Dim1() const {
++ return int_mode_ ? wi_.dim1() : wf_.dim1();
++ }
++ int Dim2() const {
++ return int_mode_ ? wi_.dim2() : wf_.dim2();
++ }
+ // Provides one set of weights. Only used by peep weight maxpool.
+ const TFloat *GetWeights(int index) const {
+ return wf_[index];
+diff -rupN --no-dereference tesseract-5.5.3/unittest/fullyconnected_test.cc tesseract-5.5.3-new/unittest/fullyconnected_test.cc
+--- tesseract-5.5.3/unittest/fullyconnected_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/fullyconnected_test.cc 2026-09-21 10:28:10.199199148 +0200
+@@ -0,0 +1,115 @@
++///////////////////////////////////////////////////////////////////////
++// File: fullyconnected_test.cc
++// Description: Tests that a FullyConnected (softmax) network layer with
++// weight-matrix dimensions that do not match the declared
++// ni/no is rejected at load. Without the check,
++// MatrixDotVector writes w.dim1() results into a scratch
++// buffer sized from no_ and reads w.dim2()-1 inputs from
++// a buffer sized from ni_ (heap out-of-bounds write/read)
++// on the first recognition step.
++//
++// 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 "network.h" // for Network, NetworkType
++#include "networkio.h"
++#include "networkscratch.h"
++#include "serialis.h" // for TFile
++
++#include <cstdint>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// 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)); }
++ const std::vector<char> &data() const { return data_; }
++
++private:
++ std::vector<char> data_;
++};
++
++void PutDoubleLE(ByteWriter *w, double d) {
++ union {
++ double d;
++ uint64_t u;
++ } conv;
++ conv.d = d;
++ w->PutU32(static_cast<uint32_t>(conv.u & 0xFFFFFFFF));
++ w->PutU32(static_cast<uint32_t>(conv.u >> 32));
++}
++
++// Builds a serialized NT_SOFTMAX network: header with the given ni/no,
++// then a float-mode WeightMatrix with the given (corrupt) dimensions.
++// The matrix is stored as doubles on disk (see WeightMatrix::DeSerialize).
++std::vector<char> MakeSoftmaxNetwork(int ni, int no, int32_t dim1, int32_t dim2) {
++ ByteWriter w;
++ w.PutU8(static_cast<uint32_t>(NT_SOFTMAX));
++ w.PutU8(0); // training: TS_DISABLED
++ w.PutU8(0); // needs_to_backprop
++ w.PutU32(0); // network_flags
++ w.PutU32(static_cast<uint32_t>(ni));
++ w.PutU32(static_cast<uint32_t>(no));
++ w.PutU32(0); // num_weights (not cross-checked, kept consistent anyway)
++ w.PutU32(0); // name (empty string)
++ // WeightMatrix::DeSerialize:
++ w.PutU8(128); // mode: kDoubleFlag, float mode
++ w.PutS32(dim1);
++ w.PutS32(dim2);
++ PutDoubleLE(&w, 0.0); // empty_ cell
++ for (int32_t i = 0; i < dim1 * dim2; ++i) {
++ PutDoubleLE(&w, 0.0); // weight data
++ }
++ return w.data();
++}
++
++// A FullyConnected layer whose weight matrix does not match the declared
++// sizes must be rejected by CreateFromFile; on unpatched code the test
++// reaches Forward, where MatrixDotVector performs the out-of-bounds
++// write this regression test guards against.
++TEST(FullyconnectedTest, RejectsWeightMatrixDimensionMismatch) {
++ // ni_=no_=1 but dim1=3 (OOB write of 3 results into a 1-result buffer)
++ // and dim2=5 (OOB read of 4 inputs from a 1-input buffer).
++ std::vector<char> bytes = MakeSoftmaxNetwork(1, 1, 3, 5);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the mismatched layer is rejected at load.
++ }
++ NetworkIO input;
++ input.Resize2d(false, /*width=*/1, /*num_features=*/1);
++ NetworkScratch scratch;
++ NetworkIO output;
++ net->Forward(false, input, nullptr, &scratch, &output);
++ delete net;
++ FAIL() << "crafted FullyConnected layer with mismatched weight matrix was accepted";
++}
++
++// Consistent dimensions must still be accepted.
++TEST(FullyconnectedTest, AcceptsMatchingDimensions) {
++ std::vector<char> bytes = MakeSoftmaxNetwork(1, 2, 2, 2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ ASSERT_NE(net, nullptr);
++ delete net;
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/1bda5079b1c8a7e25f523486837426903d29ce84.patch b/1bda5079b1c8a7e25f523486837426903d29ce84.patch
new file mode 100644
index 0000000..27053bf
--- /dev/null
+++ b/1bda5079b1c8a7e25f523486837426903d29ce84.patch
@@ -0,0 +1,171 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.150479369 +0200
+@@ -1182,6 +1182,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
+@@ -1376,6 +1377,10 @@ networkio_test_SOURCES = unittest/networ
+ 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 -rupN --no-dereference tesseract-5.5.3/src/classify/normmatch.cpp tesseract-5.5.3-new/src/classify/normmatch.cpp
+--- tesseract-5.5.3/src/classify/normmatch.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/normmatch.cpp 2026-09-21 10:28:10.151004365 +0200
+@@ -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(TF
+ 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 -rupN --no-dereference tesseract-5.5.3/unittest/CMakeLists.txt tesseract-5.5.3-new/unittest/CMakeLists.txt
+--- tesseract-5.5.3/unittest/CMakeLists.txt 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/unittest/CMakeLists.txt 2026-09-21 10:28:10.151271423 +0200
+@@ -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 -rupN --no-dereference tesseract-5.5.3/unittest/normproto_test.cc tesseract-5.5.3-new/unittest/normproto_test.cc
+--- tesseract-5.5.3/unittest/normproto_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/normproto_test.cc 2026-09-21 10:28:10.151470054 +0200
+@@ -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
diff --git a/2d04d640db2e8c7e3bab2369d599343b5a8b8443.patch b/2d04d640db2e8c7e3bab2369d599343b5a8b8443.patch
new file mode 100644
index 0000000..944d666
--- /dev/null
+++ b/2d04d640db2e8c7e3bab2369d599343b5a8b8443.patch
@@ -0,0 +1,119 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.382307312 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.387606517 +0200
+@@ -1225,6 +1225,7 @@ check_PROGRAMS += tfile_test
+ if ENABLE_TRAINING
+ check_PROGRAMS += unichar_test
+ check_PROGRAMS += unicharcompress_test
++check_PROGRAMS += unicharset_load_test
+ check_PROGRAMS += unicharset_test
+ check_PROGRAMS += validate_grapheme_test
+ check_PROGRAMS += validate_indic_test
+@@ -1518,6 +1519,10 @@ unicharcompress_test_SOURCES = unittest/
+ unicharcompress_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ unicharcompress_test_LDADD = $(TRAINING_LIBS) $(ICU_UC_LIBS)
+
++unicharset_load_test_SOURCES = unittest/unicharset_load_test.cc
++unicharset_load_test_CPPFLAGS = $(unittest_CPPFLAGS)
++unicharset_load_test_LDADD = $(TESS_LIBS)
++
+ unicharset_test_SOURCES = unittest/unicharset_test.cc
+ unicharset_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ unicharset_test_LDADD = $(TRAINING_LIBS) $(ICU_UC_LIBS)
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/unicharset.cpp tesseract-5.5.3-new/src/ccutil/unicharset.cpp
+--- tesseract-5.5.3/src/ccutil/unicharset.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/unicharset.cpp 2026-09-21 10:28:10.388227759 +0200
+@@ -791,6 +791,9 @@ bool UNICHARSET::load_via_fgets(
+ sscanf(buffer, "%d", &unicharset_size) != 1) {
+ return false;
+ }
++ if (unicharset_size <= 0) {
++ return false;
++ }
+ for (UNICHAR_ID id = 0; id < unicharset_size; ++id) {
+ char unichar[256];
+ unsigned int properties;
+@@ -884,6 +887,15 @@ bool UNICHARSET::load_via_fgets(
+ } else {
+ this->unichar_insert_backwards_compatible(unichar);
+ }
++ // A duplicate or empty representation makes the insert a no-op,
++ // desynchronizing id from the unichars vector; the set_* calls and
++ // unichars[id] below would then write out of bounds. The file is
++ // malformed, so reject it.
++ if (size() != static_cast<size_t>(id) + 1) {
++ fprintf(stderr, "%s:%d unichar %d has a duplicate or empty representation\n",
++ __FILE__, __LINE__, id);
++ return false;
++ }
+
+ this->set_isalpha(id, properties & ISALPHA_MASK);
+ this->set_islower(id, properties & ISLOWER_MASK);
+diff -rupN --no-dereference tesseract-5.5.3/unittest/unicharset_load_test.cc tesseract-5.5.3-new/unittest/unicharset_load_test.cc
+--- tesseract-5.5.3/unittest/unicharset_load_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/unicharset_load_test.cc 2026-09-21 10:28:10.388594394 +0200
+@@ -0,0 +1,64 @@
++///////////////////////////////////////////////////////////////////////
++// File: unicharset_load_test.cc
++// Description: Tests that UNICHARSET::load_via_fgets rejects unicharset
++// files whose insertions desynchronize the id loop index
++// from the unichars vector (duplicate or empty
++// representations), which would make the subsequent set_*
++// calls write out of bounds, and non-positive size counts.
++//
++// 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 "serialis.h" // for TFile
++#include "unicharset.h"
++
++#include <cstring>
++
++namespace tesseract {
++namespace {
++
++// Loads the given unicharset text via the TFile-based loader.
++bool LoadUnicharset(const char *text, UNICHARSET *unicharset) {
++ TFile fp;
++ if (!fp.Open(text, std::strlen(text))) {
++ return false;
++ }
++ return unicharset->load_from_file(&fp, false);
++}
++
++// A duplicate representation makes the second insert a no-op, so on
++// unpatched code the set_* calls for the remaining lines write past
++// the end of the unichars vector (ASan container-overflow).
++TEST(UnicharsetLoadTest, RejectsDuplicateRepresentation) {
++ const char *text = "3\nA 0 Latin\nA 0 Latin\nB 0 Latin\n";
++ UNICHARSET unicharset;
++ EXPECT_FALSE(LoadUnicharset(text, &unicharset));
++}
++
++// A non-positive size count must be rejected; on unpatched code a
++// zero or negative count loads an empty unicharset successfully.
++TEST(UnicharsetLoadTest, RejectsNonPositiveCount) {
++ const char *texts[] = {"0\n", "-1\n"};
++ for (const char *text : texts) {
++ UNICHARSET unicharset;
++ EXPECT_FALSE(LoadUnicharset(text, &unicharset));
++ }
++}
++
++// A valid unicharset must still be accepted.
++TEST(UnicharsetLoadTest, AcceptsValidUnicharset) {
++ const char *text = "3\nA 0 Latin\nB 0 Latin\nC 0 Latin\n";
++ UNICHARSET unicharset;
++ ASSERT_TRUE(LoadUnicharset(text, &unicharset));
++ EXPECT_EQ(unicharset.size(), 3u);
++ EXPECT_STREQ(unicharset.id_to_unichar(1), "B");
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/552771236b0d80cbdb0c7dd856120fa21a4672e5.patch b/552771236b0d80cbdb0c7dd856120fa21a4672e5.patch
new file mode 100644
index 0000000..fe0ad93
--- /dev/null
+++ b/552771236b0d80cbdb0c7dd856120fa21a4672e5.patch
@@ -0,0 +1,224 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.490334879 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.500544004 +0200
+@@ -1199,6 +1199,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
+@@ -1434,6 +1435,10 @@ paragraphs_test_SOURCES = unittest/parag
+ 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 -rupN --no-dereference tesseract-5.5.3/src/ccmain/tessedit.cpp tesseract-5.5.3-new/src/ccmain/tessedit.cpp
+--- tesseract-5.5.3/src/ccmain/tessedit.cpp 2026-09-21 10:28:10.490847641 +0200
++++ tesseract-5.5.3-new/src/ccmain/tessedit.cpp 2026-09-21 10:28:10.500920172 +0200
+@@ -169,7 +169,12 @@ bool Tesseract::init_tesseract_lang_data
+ #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 -rupN --no-dereference tesseract-5.5.3/src/lstm/plumbing.cpp tesseract-5.5.3-new/src/lstm/plumbing.cpp
+--- tesseract-5.5.3/src/lstm/plumbing.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/lstm/plumbing.cpp 2026-09-21 10:28:10.501133233 +0200
+@@ -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 -rupN --no-dereference tesseract-5.5.3/unittest/plumbing_test.cc tesseract-5.5.3-new/unittest/plumbing_test.cc
+--- tesseract-5.5.3/unittest/plumbing_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/plumbing_test.cc 2026-09-21 10:28:10.501347655 +0200
+@@ -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
diff --git a/56e09ca12e751623fe796ce1554ce704bffd2ef0.patch b/56e09ca12e751623fe796ce1554ce704bffd2ef0.patch
new file mode 100644
index 0000000..51664b1
--- /dev/null
+++ b/56e09ca12e751623fe796ce1554ce704bffd2ef0.patch
@@ -0,0 +1,141 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.335473657 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.340990106 +0200
+@@ -1159,6 +1159,7 @@ check_PROGRAMS += equationdetect_test
+ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += fileio_test
+ check_PROGRAMS += fullyconnected_test
++check_PROGRAMS += genericvector_test
+ check_PROGRAMS += heap_test
+ check_PROGRAMS += imagedata_test
+ if !DISABLED_LEGACY_ENGINE
+@@ -1297,6 +1298,10 @@ fullyconnected_test_SOURCES = unittest/f
+ fullyconnected_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ fullyconnected_test_LDADD = $(TESS_LIBS)
+
++genericvector_test_SOURCES = unittest/genericvector_test.cc
++genericvector_test_CPPFLAGS = $(unittest_CPPFLAGS)
++genericvector_test_LDADD = $(TESS_LIBS)
++
+ heap_test_SOURCES = unittest/heap_test.cc
+ heap_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ heap_test_LDADD = $(TESS_LIBS)
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/genericvector.h tesseract-5.5.3-new/src/ccutil/genericvector.h
+--- tesseract-5.5.3/src/ccutil/genericvector.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/genericvector.h 2026-09-21 10:28:10.341648838 +0200
+@@ -654,10 +654,20 @@ bool GenericVector<T>::read(TFile *f, co
+ if (f->FReadEndian(&reserved, sizeof(reserved), 1) != 1) {
+ return false;
+ }
++ // Arbitrarily limit the number of elements to protect against bad data.
++ const uint32_t limit = 50000000;
++ if (reserved < 0 || static_cast<uint32_t>(reserved) > limit) {
++ return false;
++ }
+ reserve(reserved);
+ if (f->FReadEndian(&size_used_, sizeof(size_used_), 1) != 1) {
+ return false;
+ }
++ // size_used_ is an independent file field; without this check the reads
++ // below land past the end of the buffer sized from reserved.
++ if (size_used_ < 0 || size_used_ > reserved) {
++ return false;
++ }
+ if (cb != nullptr) {
+ for (int i = 0; i < size_used_; ++i) {
+ if (!cb(f, data_ + i)) {
+diff -rupN --no-dereference tesseract-5.5.3/unittest/genericvector_test.cc tesseract-5.5.3-new/unittest/genericvector_test.cc
+--- tesseract-5.5.3/unittest/genericvector_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/genericvector_test.cc 2026-09-21 10:28:10.342013484 +0200
+@@ -0,0 +1,91 @@
++///////////////////////////////////////////////////////////////////////
++// File: genericvector_test.cc
++// Description: Tests that the callback form of GenericVector::read
++// rejects vectors whose size_used_ exceeds reserved (or
++// whose counts are negative). reserved sizes the buffer
++// while size_used_ is an independent file field driving
++// the element loop, so a crafted .traineddata (e.g. the
++// fontinfo table of a version >= 4 inttemp component)
++// performs a heap out-of-bounds write 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 "genericvector.h"
++#include "serialis.h" // for TFile
++
++#include <cstdint>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// Appends raw little-endian values to a byte buffer.
++class ByteWriter {
++public:
++ void PutS32(int32_t v) {
++ uint32_t u = static_cast<uint32_t>(v);
++ for (int i = 0; i < 4; ++i) {
++ data_.push_back(static_cast<char>((u >> (8 * i)) & 0xFF));
++ }
++ }
++ const std::vector<char> &data() const { return data_; }
++
++private:
++ std::vector<char> data_;
++};
++
++// A serialized vector header (reserved, size_used_) followed by the
++// given number of int32 elements.
++std::vector<char> MakeVector(int32_t reserved, int32_t size_used, int32_t num_elements) {
++ ByteWriter w;
++ w.PutS32(reserved);
++ w.PutS32(size_used);
++ for (int32_t i = 0; i < num_elements; ++i) {
++ w.PutS32(i);
++ }
++ return w.data();
++}
++
++// reserved=4 but size_used_=0x10000: on unpatched code the callback
++// loop writes 65536 ints past the 4-int buffer (heap out-of-bounds
++// write).
++TEST(GenericVectorTest, RejectsSizeUsedBeyondReserved) {
++ std::vector<char> bytes = MakeVector(4, 0x10000, 0x10000);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ GenericVector<int> v;
++ EXPECT_FALSE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
++}
++
++// Negative counts must be rejected; on unpatched code the read
++// "succeeds" and leaves size_used_ negative.
++TEST(GenericVectorTest, RejectsNegativeCounts) {
++ std::vector<char> bytes = MakeVector(-1, -1, 0);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ GenericVector<int> v;
++ EXPECT_FALSE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
++}
++
++// A consistent vector must still be accepted.
++TEST(GenericVectorTest, AcceptsConsistentVector) {
++ std::vector<char> bytes = MakeVector(4, 2, 2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ GenericVector<int> v;
++ ASSERT_TRUE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
++ EXPECT_EQ(v.size(), 2);
++ EXPECT_EQ(v[0], 0);
++ EXPECT_EQ(v[1], 1);
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/8b0574680f3b22f246ade6a4c8e3029104255c63.patch b/8b0574680f3b22f246ade6a4c8e3029104255c63.patch
new file mode 100644
index 0000000..f65dea9
--- /dev/null
+++ b/8b0574680f3b22f246ade6a4c8e3029104255c63.patch
@@ -0,0 +1,760 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.428681133 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.436503509 +0200
+@@ -1165,6 +1165,7 @@ check_PROGRAMS += imagedata_test
+ if !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += indexmapbidi_test
+ check_PROGRAMS += intfeaturemap_test
++check_PROGRAMS += intproto_test
+ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += intsimdmatrix_test
+ check_PROGRAMS += lang_model_test
+@@ -1323,6 +1324,12 @@ intfeaturemap_test_CPPFLAGS = $(unittest
+ intfeaturemap_test_LDADD = $(TRAINING_LIBS)
+ endif # !DISABLED_LEGACY_ENGINE
+
++if !DISABLED_LEGACY_ENGINE
++intproto_test_SOURCES = unittest/intproto_test.cc
++intproto_test_CPPFLAGS = $(unittest_CPPFLAGS)
++intproto_test_LDADD = $(TESS_LIBS)
++endif # !DISABLED_LEGACY_ENGINE
++
+ intsimdmatrix_test_SOURCES = unittest/intsimdmatrix_test.cc
+ intsimdmatrix_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ if HAVE_AVX2
+diff -rupN --no-dereference tesseract-5.5.3/src/ccmain/tessedit.cpp tesseract-5.5.3-new/src/ccmain/tessedit.cpp
+--- tesseract-5.5.3/src/ccmain/tessedit.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccmain/tessedit.cpp 2026-09-21 10:28:10.436842574 +0200
+@@ -418,7 +418,9 @@ int Tesseract::init_tesseract_internal(c
+ // If only LSTM will be used, skip loading Tesseract classifier's
+ // pre-trained templates and dictionary.
+ bool init_tesseract = tessedit_ocr_engine_mode != OEM_LSTM_ONLY;
+- program_editup(textbase, init_tesseract ? mgr : nullptr, init_tesseract ? mgr : nullptr);
++ if (!program_editup(textbase, init_tesseract ? mgr : nullptr, init_tesseract ? mgr : nullptr)) {
++ return -1;
++ }
+ return 0; // Normal exit
+ }
+
+diff -rupN --no-dereference tesseract-5.5.3/src/ccstruct/blamer.h tesseract-5.5.3-new/src/ccstruct/blamer.h
+--- tesseract-5.5.3/src/ccstruct/blamer.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccstruct/blamer.h 2026-09-21 10:28:10.434500467 +0200
+@@ -232,6 +232,7 @@ struct BlamerBundle {
+ lattice_size_ = other.lattice_size_;
+ } else {
+ lattice_data_ = nullptr;
++ lattice_size_ = 0;
+ }
+ }
+ const char *IncorrectReason() const;
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/clst.h tesseract-5.5.3-new/src/ccutil/clst.h
+--- tesseract-5.5.3/src/ccutil/clst.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/clst.h 2026-09-21 10:28:10.434932966 +0200
+@@ -146,6 +146,13 @@ public:
+ public:
+ Iterator() { // constructor
+ list = nullptr;
++ prev = nullptr;
++ current = nullptr;
++ next = nullptr;
++ cycle_pt = nullptr; // await explicit set
++ started_cycling = false;
++ ex_current_was_last = false;
++ ex_current_was_cycle_pt = false;
+ } // unassigned list
+
+ /***********************************************************************
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/elst.h tesseract-5.5.3-new/src/ccutil/elst.h
+--- tesseract-5.5.3/src/ccutil/elst.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/elst.h 2026-09-21 10:28:10.435349347 +0200
+@@ -209,6 +209,13 @@ public:
+ public:
+ Iterator() { // constructor
+ list = nullptr;
++ prev = nullptr;
++ current = nullptr;
++ next = nullptr;
++ cycle_pt = nullptr; // await explicit set
++ started_cycling = false;
++ ex_current_was_last = false;
++ ex_current_was_cycle_pt = false;
+ } // unassigned list
+ /***********************************************************************
+ * ELIST_ITERATOR::ELIST_ITERATOR
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/kdpair.h tesseract-5.5.3-new/src/ccutil/kdpair.h
+--- tesseract-5.5.3/src/ccutil/kdpair.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/kdpair.h 2026-09-21 10:28:10.435591532 +0200
+@@ -99,7 +99,7 @@ struct KDPairDec : public KDPair<Key, Da
+ template <typename Key, typename Data>
+ class KDPtrPair {
+ public:
+- KDPtrPair() : data_(nullptr) {}
++ KDPtrPair() : data_(nullptr), key_{} {}
+ KDPtrPair(Key k, Data *d) : data_(d), key_(k) {}
+ // Copy constructor steals the pointer from src and nulls it in src, thereby
+ // moving the (single) ownership of the data.
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/adaptive.cpp tesseract-5.5.3-new/src/classify/adaptive.cpp
+--- tesseract-5.5.3/src/classify/adaptive.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/adaptive.cpp 2026-09-21 10:28:10.437111722 +0200
+@@ -67,10 +67,6 @@ ADAPT_CLASS_STRUCT::ADAPT_CLASS_STRUCT()
+ TempProtos(NIL_LIST) {
+ zero_all_bits(PermProtos, WordsInVectorOfSize(MAX_NUM_PROTOS));
+ zero_all_bits(PermConfigs, WordsInVectorOfSize(MAX_NUM_CONFIGS));
+-
+- for (int i = 0; i < MAX_NUM_CONFIGS; i++) {
+- TempConfigFor(this, i) = nullptr;
+- }
+ }
+
+ ADAPT_CLASS_STRUCT::~ADAPT_CLASS_STRUCT() {
+@@ -92,25 +88,24 @@ ADAPT_CLASS_STRUCT::~ADAPT_CLASS_STRUCT(
+
+ /// Constructor for adapted templates.
+ /// Add an empty class for each char in unicharset to the newly created templates.
+-ADAPT_TEMPLATES_STRUCT::ADAPT_TEMPLATES_STRUCT(UNICHARSET &unicharset) {
+- Templates = new INT_TEMPLATES_STRUCT;
+- NumPermClasses = 0;
+- NumNonEmptyClasses = 0;
+-
+- /* Insert an empty class for each unichar id in unicharset */
+- for (unsigned i = 0; i < MAX_NUM_CLASSES; i++) {
+- Class[i] = nullptr;
+- if (i < unicharset.size()) {
+- AddAdaptedClass(this, new ADAPT_CLASS_STRUCT, i);
+- }
++ADAPT_TEMPLATES_STRUCT::ADAPT_TEMPLATES_STRUCT(UNICHARSET &unicharset) :
++ Templates(new INT_TEMPLATES_STRUCT), NumNonEmptyClasses(0), NumPermClasses(0) {
++ // Insert an empty class for each unichar id in unicharset.
++ // Class is value-initialized to nullptr in-class.
++ for (unsigned i = 0; i < unicharset.size(); i++) {
++ AddAdaptedClass(this, new ADAPT_CLASS_STRUCT, i);
+ }
+ }
+
+ ADAPT_TEMPLATES_STRUCT::~ADAPT_TEMPLATES_STRUCT() {
+- for (unsigned i = 0; i < (Templates)->NumClasses; i++) {
+- delete Class[i];
++ if (Templates != nullptr) {
++ // NumClasses comes from an untrusted file, so never trust it to bound
++ // the loop over the fixed-size Class[] array.
++ for (unsigned i = 0; i < (Templates)->NumClasses && i < MAX_NUM_CLASSES; i++) {
++ delete Class[i];
++ }
++ delete Templates;
+ }
+- delete Templates;
+ }
+
+ // Returns FontinfoId of the given config of the given adapted class.
+@@ -180,23 +175,32 @@ void Classify::PrintAdaptedTemplates(FIL
+ * @note Globals: none
+ */
+ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFile *fp) {
+- int NumTempProtos;
+- int NumConfigs;
++ int NumTempProtos = 0;
++ int NumConfigs = 0;
+ int i;
+ ADAPT_CLASS_STRUCT *Class;
+
+- /* first read high level adapted class structure */
++ // first read high level adapted class structure
+ Class = new ADAPT_CLASS_STRUCT;
+ fp->FRead(Class, sizeof(ADAPT_CLASS_STRUCT), 1);
+
+- /* then read in the definitions of the permanent protos and configs */
++ // then read in the definitions of the permanent protos and configs
+ Class->PermProtos = NewBitVector(MAX_NUM_PROTOS);
+ Class->PermConfigs = NewBitVector(MAX_NUM_CONFIGS);
+ fp->FRead(Class->PermProtos, sizeof(uint32_t), WordsInVectorOfSize(MAX_NUM_PROTOS));
+ fp->FRead(Class->PermConfigs, sizeof(uint32_t), WordsInVectorOfSize(MAX_NUM_CONFIGS));
+
+- /* then read in the list of temporary protos */
++ // then read in the list of temporary protos
+ fp->FRead(&NumTempProtos, sizeof(int), 1);
++ if (NumTempProtos < 0 || NumTempProtos > MAX_NUM_PROTOS) {
++ tprintf("Bad read of adapted class!\n");
++ // Reset file-sourced pointers so the destructor does not delete them.
++ for (i = 0; i < MAX_NUM_CONFIGS; i++) {
++ Class->Config[i].Temp = nullptr;
++ }
++ delete Class;
++ return nullptr;
++ }
+ Class->TempProtos = NIL_LIST;
+ for (i = 0; i < NumTempProtos; i++) {
+ auto TempProto = new TEMP_PROTO_STRUCT;
+@@ -204,8 +208,20 @@ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFi
+ Class->TempProtos = push_last(Class->TempProtos, TempProto);
+ }
+
+- /* then read in the adapted configs */
++ // then read in the adapted configs
+ fp->FRead(&NumConfigs, sizeof(int), 1);
++ // NumConfigs is used as a loop bound that writes into the fixed-size
++ // Config[] array, so reject a corrupt or malicious file instead of
++ // writing out of bounds.
++ if (NumConfigs < 0 || NumConfigs > MAX_NUM_CONFIGS) {
++ tprintf("Bad read of adapted class!\n");
++ // Reset file-sourced pointers so the destructor does not delete them.
++ for (i = 0; i < MAX_NUM_CONFIGS; i++) {
++ Class->Config[i].Temp = nullptr;
++ }
++ delete Class;
++ return nullptr;
++ }
+ for (i = 0; i < NumConfigs; i++) {
+ if (test_bit(Class->PermConfigs, i)) {
+ Class->Config[i].Perm = ReadPermConfig(fp);
+@@ -231,18 +247,35 @@ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFi
+ ADAPT_TEMPLATES_STRUCT *Classify::ReadAdaptedTemplates(TFile *fp) {
+ auto Templates = new ADAPT_TEMPLATES_STRUCT;
+
+- /* first read the high level adaptive template struct */
+- fp->FRead(Templates, sizeof(ADAPT_TEMPLATES_STRUCT), 1);
++ // first read in the high level adaptive template struct
++ if (fp->FRead(Templates, sizeof(ADAPT_TEMPLATES_STRUCT), 1) != 1) {
++ tprintf("Bad read of adapted templates!\n");
++ delete Templates;
++ return nullptr;
++ }
++ // The Class[] array was just filled with pointers read from the file;
++ // those are not valid allocations, so reset it before storing real ones.
++ for (unsigned i = 0; i < MAX_NUM_CLASSES; i++) {
++ Templates->Class[i] = nullptr;
++ }
+
+- /* then read in the basic integer templates */
++ // then read in the basic integer templates
+ Templates->Templates = ReadIntTemplates(fp);
++ if (Templates->Templates == nullptr) {
++ delete Templates;
++ return nullptr;
++ }
+
+- /* then read in the adaptive info for each class */
++ // then read in the adaptive info for each class
+ for (unsigned i = 0; i < (Templates->Templates)->NumClasses; i++) {
+ Templates->Class[i] = ReadAdaptedClass(fp);
++ if (Templates->Class[i] == nullptr) {
++ tprintf("Bad read of adapted templates (class %u)!\n", i);
++ delete Templates;
++ return nullptr;
++ }
+ }
+ return (Templates);
+-
+ } /* ReadAdaptedTemplates */
+
+ /*---------------------------------------------------------------------------*/
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/adaptive.h tesseract-5.5.3-new/src/classify/adaptive.h
+--- tesseract-5.5.3/src/classify/adaptive.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/adaptive.h 2026-09-21 10:28:10.437339390 +0200
+@@ -20,6 +20,7 @@
+ #include "intproto.h"
+ #include "oldlist.h"
+
++#include <array>
+ #include <cstdio>
+
+ namespace tesseract {
+@@ -61,18 +62,18 @@ struct ADAPT_CLASS_STRUCT {
+ BIT_VECTOR PermProtos;
+ BIT_VECTOR PermConfigs;
+ LIST TempProtos;
+- ADAPTED_CONFIG Config[MAX_NUM_CONFIGS];
++ std::array<ADAPTED_CONFIG, MAX_NUM_CONFIGS> Config{};
+ };
+
+ class ADAPT_TEMPLATES_STRUCT {
+ public:
+- ADAPT_TEMPLATES_STRUCT() = default;
++ ADAPT_TEMPLATES_STRUCT() : Templates(nullptr), NumNonEmptyClasses(0), NumPermClasses(0) {}
+ ADAPT_TEMPLATES_STRUCT(UNICHARSET &unicharset);
+ ~ADAPT_TEMPLATES_STRUCT();
+ INT_TEMPLATES_STRUCT *Templates;
+ int NumNonEmptyClasses;
+ uint8_t NumPermClasses;
+- ADAPT_CLASS_STRUCT *Class[MAX_NUM_CLASSES];
++ std::array<ADAPT_CLASS_STRUCT *, MAX_NUM_CLASSES> Class{};
+ };
+
+ /*----------------------------------------------------------------------------
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/adaptmatch.cpp tesseract-5.5.3-new/src/classify/adaptmatch.cpp
+--- tesseract-5.5.3/src/classify/adaptmatch.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/adaptmatch.cpp 2026-09-21 10:28:10.437616318 +0200
+@@ -524,9 +524,9 @@ void Classify::EndAdaptiveClassifier() {
+ * classify_use_pre_adapted_templates
+ * enables use of pre-adapted templates
+ */
+-void Classify::InitAdaptiveClassifier(TessdataManager *mgr) {
++bool Classify::InitAdaptiveClassifier(TessdataManager *mgr) {
+ if (!CLASSIFY_ENABLE_ADAPTIVE_MATCHER_OVERRIDE) {
+- return;
++ return true;
+ }
+ if (AllProtosOn != nullptr) {
+ EndAdaptiveClassifier(); // Don't leak with multiple inits.
+@@ -538,6 +538,11 @@ void Classify::InitAdaptiveClassifier(Te
+ TFile fp;
+ ASSERT_HOST(mgr->GetComponent(TESSDATA_INTTEMP, &fp));
+ PreTrainedTemplates = ReadIntTemplates(&fp);
++ if (PreTrainedTemplates == nullptr) {
++ tprintf("Error: invalid inttemp component in traineddata, "
++ "cannot initialize the legacy engine.\n");
++ return false;
++ }
+
+ if (mgr->GetComponent(TESSDATA_SHAPE_TABLE, &fp)) {
+ shape_table_ = new ShapeTable(unicharset);
+@@ -580,17 +585,23 @@ void Classify::InitAdaptiveClassifier(Te
+ tprintf("\nReading pre-adapted templates from %s ...\n", Filename.c_str());
+ fflush(stdout);
+ AdaptedTemplates = ReadAdaptedTemplates(&fp);
+- tprintf("\n");
+- PrintAdaptedTemplates(stdout, AdaptedTemplates);
++ if (AdaptedTemplates == nullptr) {
++ tprintf("Error: invalid pre-adapted templates in %s, ignoring.\n", Filename.c_str());
++ AdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset);
++ } else {
++ tprintf("\n");
++ PrintAdaptedTemplates(stdout, AdaptedTemplates);
+
+- for (unsigned i = 0; i < AdaptedTemplates->Templates->NumClasses; i++) {
+- BaselineCutoffs[i] = CharNormCutoffs[i];
++ for (unsigned i = 0; i < AdaptedTemplates->Templates->NumClasses; i++) {
++ BaselineCutoffs[i] = CharNormCutoffs[i];
++ }
+ }
+ }
+ } else {
+ delete AdaptedTemplates;
+ AdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset);
+ }
++ return true;
+ } /* InitAdaptiveClassifier */
+
+ void Classify::ResetAdaptiveClassifierInternal() {
+@@ -1240,8 +1251,8 @@ UNICHAR_ID *Classify::BaselineClassifier
+ }
+
+ MasterMatcher(Templates->Templates, int_features.size(), &int_features[0], CharNormArray,
+- Templates->Class, matcher_debug_flags, 0, Blob->bounding_box(), Results->CPResults,
+- Results);
++ Templates->Class.data(), matcher_debug_flags, 0, Blob->bounding_box(),
++ Results->CPResults, Results);
+
+ delete[] CharNormArray;
+ CLASS_ID ClassId = Results->best_unichar_id;
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/classify.h tesseract-5.5.3-new/src/classify/classify.h
+--- tesseract-5.5.3/src/classify/classify.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/classify.h 2026-09-21 10:28:10.438050462 +0200
+@@ -164,7 +164,7 @@ public:
+ // provided to explicitly clarify the character segmentation.
+ void LearnPieces(const char *fontname, int start, int length, float threshold,
+ CharSegmentationType segmentation, const char *correct_text, WERD_RES *word);
+- void InitAdaptiveClassifier(TessdataManager *mgr);
++ bool InitAdaptiveClassifier(TessdataManager *mgr);
+ void InitAdaptedClass(TBLOB *Blob, CLASS_ID ClassId, int FontinfoId, ADAPT_CLASS_STRUCT *Class,
+ ADAPT_TEMPLATES_STRUCT *Templates);
+ void AmbigClassifier(const std::vector<INT_FEATURE_STRUCT> &int_features,
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/cluster.h tesseract-5.5.3-new/src/classify/cluster.h
+--- tesseract-5.5.3/src/classify/cluster.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/cluster.h 2026-09-21 10:28:10.435765974 +0200
+@@ -32,7 +32,7 @@ constexpr int MAXBUCKETS = 39;
+ Types
+ ----------------------------------------------------------------------*/
+ struct CLUSTER {
+- CLUSTER(size_t n) : Mean(n) {
++ CLUSTER(size_t n) : Left(nullptr), Right(nullptr), Mean(n) {
+ }
+
+ ~CLUSTER() {
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/featdefs.h tesseract-5.5.3-new/src/classify/featdefs.h
+--- tesseract-5.5.3/src/classify/featdefs.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/featdefs.h 2026-09-21 10:28:10.435932039 +0200
+@@ -47,9 +47,8 @@ using FEATURE_DEFS = FEATURE_DEFS_STRUCT
+ struct CHAR_DESC_STRUCT {
+ /// Allocate a new character description, initialize its
+ /// feature sets to be empty, and return it.
+- CHAR_DESC_STRUCT(const FEATURE_DEFS_STRUCT &FeatureDefs) {
+- NumFeatureSets = FeatureDefs.NumFeatureTypes;
+- }
++ CHAR_DESC_STRUCT(const FEATURE_DEFS_STRUCT &FeatureDefs)
++ : NumFeatureSets(FeatureDefs.NumFeatureTypes), FeatureSets{} {}
+
+ /// Release the memory consumed by the specified character
+ /// description and all of the features in that description.
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/intproto.cpp tesseract-5.5.3-new/src/classify/intproto.cpp
+--- tesseract-5.5.3/src/classify/intproto.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/intproto.cpp 2026-09-21 10:28:10.439253621 +0200
+@@ -286,11 +286,10 @@ int AddIntProto(INT_CLASS_STRUCT *Class)
+ Class->ProtoLengths.resize(MaxNumIntProtosIn(Class));
+ }
+
+- /* initialize proto so its length is zero and it isn't in any configs */
++ // initialize proto so its length is zero and it isn't in any configs
+ Class->ProtoLengths[Index] = 0;
+ auto Proto = ProtoForProtoId(Class, Index);
+- for (uint32_t *Word = Proto->Configs; Word < Proto->Configs + WERDS_PER_CONFIG_VEC; *Word++ = 0) {
+- }
++ Proto->Configs.fill(0);
+
+ return (Index);
+ }
+@@ -583,38 +582,36 @@ INT_CLASS_STRUCT::INT_CLASS_STRUCT(int M
+ assert(NumProtoSets <= MAX_NUM_PROTO_SETS);
+
+ for (int i = 0; i < NumProtoSets; i++) {
+- /* allocate space for a proto set, install in class, and initialize */
++ // allocate space for a proto set, install in class, and initialize
+ auto ProtoSet = new PROTO_SET_STRUCT;
+ memset(ProtoSet, 0, sizeof(*ProtoSet));
+ ProtoSets[i] = ProtoSet;
+
+- /* allocate space for the proto lengths and install in class */
++ // allocate space for the proto lengths and install in class
+ }
+- memset(ConfigLengths, 0, sizeof(ConfigLengths));
+ }
+
+ INT_CLASS_STRUCT::~INT_CLASS_STRUCT() {
+- for (int i = 0; i < NumProtoSets; i++) {
++ // NumProtoSets comes from an untrusted file, so never trust it to bound
++ // the loop over the fixed-size ProtoSets[] array.
++ for (int i = 0; i < NumProtoSets && i < MAX_NUM_PROTO_SETS; i++) {
+ delete ProtoSets[i];
+ }
+ }
+
+ /// This constructor allocates a new set of integer templates
+ /// initialized to hold 0 classes.
+-INT_TEMPLATES_STRUCT::INT_TEMPLATES_STRUCT() {
+- NumClasses = 0;
+- NumClassPruners = 0;
+-
+- for (int i = 0; i < MAX_NUM_CLASSES; i++) {
+- ClassForClassId(this, i) = nullptr;
+- }
++INT_TEMPLATES_STRUCT::INT_TEMPLATES_STRUCT() : NumClasses(0), NumClassPruners(0) {
++ // Class and ClassPruners are value-initialized to nullptr in-class.
+ }
+
+ INT_TEMPLATES_STRUCT::~INT_TEMPLATES_STRUCT() {
+- for (unsigned i = 0; i < NumClasses; i++) {
++ // The counts come from an untrusted file, so never trust them to bound
++ // the loops over the fixed-size arrays.
++ for (unsigned i = 0; i < NumClasses && i < MAX_NUM_CLASSES; i++) {
+ delete Class[i];
+ }
+- for (unsigned i = 0; i < NumClassPruners; i++) {
++ for (unsigned i = 0; i < NumClassPruners && i < MAX_NUM_CLASS_PRUNERS; i++) {
+ delete ClassPruners[i];
+ }
+ }
+@@ -666,6 +663,20 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntT
+ Templates->NumClasses = version_id;
+ }
+
++ // The counts read from the file are used as loop bounds that write into
++ // fixed-size arrays (Class[], ClassPruners[], TempClassPruner[] and
++ // IndexFor[]), so reject a corrupt or malicious file instead of writing
++ // out of bounds.
++ if (unicharset_size > MAX_NUM_CLASSES ||
++ Templates->NumClassPruners > MAX_NUM_CLASS_PRUNERS ||
++ Templates->NumClasses > MAX_NUM_CLASSES) {
++ tprintf("Error: invalid counts in inttemp: unicharset_size=%u, NumClassPruners=%u, "
++ "NumClasses=%u\n",
++ unicharset_size, Templates->NumClassPruners, Templates->NumClasses);
++ delete Templates;
++ return nullptr;
++ }
++
+ if (version_id < 3) {
+ MaxNumConfigs = OLD_MAX_NUM_CONFIGS;
+ WerdsPerConfigVec = OLD_WERDS_PER_CONFIG_VEC;
+@@ -705,6 +716,16 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntT
+ max_class_id = ClassIdFor[i];
+ }
+ }
++ // Class ids index Class[] and (divided by CLASSES_PER_CP) ClassPruners[],
++ // so reject a corrupt or malicious file instead of writing out of bounds.
++ if (max_class_id >= MAX_NUM_CLASSES) {
++ tprintf("Error: class id %u in inttemp exceeds MAX_NUM_CLASSES\n", max_class_id);
++ for (unsigned i = 0; i < Templates->NumClassPruners; i++) {
++ delete TempClassPruner[i];
++ }
++ delete Templates;
++ return nullptr;
++ }
+ for (int i = 0; i <= CPrunerIdFor(max_class_id); i++) {
+ Templates->ClassPruners[i] = new CLASS_PRUNER_STRUCT;
+ memset(Templates->ClassPruners[i], 0, sizeof(CLASS_PRUNER_STRUCT));
+@@ -774,8 +795,20 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntT
+ }
+ }
+ unsigned num_configs = version_id < 4 ? MaxNumConfigs : Class->NumConfigs;
+- ASSERT_HOST(num_configs <= MaxNumConfigs);
+- if (fp->FReadEndian(Class->ConfigLengths, sizeof(uint16_t), num_configs) != num_configs) {
++ // Class->NumProtoSets is used as a loop bound that writes into the
++ // fixed-size ProtoSets[] array, so reject a corrupt or malicious file
++ // instead of writing out of bounds.
++ if (Class->NumProtos > MAX_NUM_PROTOS || Class->NumProtoSets > MAX_NUM_PROTO_SETS ||
++ num_configs > MaxNumConfigs) {
++ tprintf("Error: invalid counts for class %u in inttemp: NumProtos=%u, NumProtoSets=%u, "
++ "NumConfigs=%u\n",
++ i, Class->NumProtos, Class->NumProtoSets, Class->NumConfigs);
++ Class->NumProtoSets = 0; // no proto sets allocated yet; keep destructor safe
++ delete Class;
++ delete Templates;
++ return nullptr;
++ }
++ if (fp->FReadEndian(Class->ConfigLengths.data(), sizeof(uint16_t), num_configs) != num_configs) {
+ tprintf("Bad read of inttemp!\n");
+ }
+ if (version_id < 2) {
+@@ -809,8 +842,9 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntT
+ fp->FRead(&ProtoSet->Protos[x].Angle, sizeof(ProtoSet->Protos[x].Angle), 1) != 1) {
+ tprintf("Bad read of inttemp!\n");
+ }
+- if (fp->FReadEndian(&ProtoSet->Protos[x].Configs, sizeof(ProtoSet->Protos[x].Configs[0]),
+- WerdsPerConfigVec) != WerdsPerConfigVec) {
++ if (fp->FReadEndian(ProtoSet->Protos[x].Configs.data(),
++ sizeof(ProtoSet->Protos[x].Configs[0]), WerdsPerConfigVec) !=
++ WerdsPerConfigVec) {
+ tprintf("Bad read of inttemp!\n");
+ }
+ }
+diff -rupN --no-dereference tesseract-5.5.3/src/classify/intproto.h tesseract-5.5.3-new/src/classify/intproto.h
+--- tesseract-5.5.3/src/classify/intproto.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/classify/intproto.h 2026-09-21 10:28:10.439709784 +0200
+@@ -80,14 +80,14 @@ struct INT_PROTO_STRUCT {
+ uint8_t B;
+ int8_t C;
+ uint8_t Angle;
+- uint32_t Configs[WERDS_PER_CONFIG_VEC];
++ std::array<uint32_t, WERDS_PER_CONFIG_VEC> Configs{};
+ };
+
+ typedef uint32_t PROTO_PRUNER[NUM_PP_PARAMS][NUM_PP_BUCKETS][WERDS_PER_PP_VECTOR];
+
+ struct PROTO_SET_STRUCT {
+ PROTO_PRUNER ProtoPruner;
+- INT_PROTO_STRUCT Protos[PROTOS_PER_PROTO_SET];
++ std::array<INT_PROTO_STRUCT, PROTOS_PER_PROTO_SET> Protos{};
+ };
+
+ typedef uint32_t CONFIG_PRUNER[NUM_PP_PARAMS][NUM_PP_BUCKETS][4];
+@@ -99,9 +99,9 @@ struct INT_CLASS_STRUCT {
+ uint16_t NumProtos = 0;
+ uint8_t NumProtoSets = 0;
+ uint8_t NumConfigs = 0;
+- PROTO_SET_STRUCT *ProtoSets[MAX_NUM_PROTO_SETS];
++ std::array<PROTO_SET_STRUCT *, MAX_NUM_PROTO_SETS> ProtoSets{};
+ std::vector<uint8_t> ProtoLengths;
+- uint16_t ConfigLengths[MAX_NUM_CONFIGS];
++ std::array<uint16_t, MAX_NUM_CONFIGS> ConfigLengths{};
+ int font_set_id = 0; // FontSet id, see above
+ };
+
+@@ -110,8 +110,8 @@ struct TESS_API INT_TEMPLATES_STRUCT {
+ ~INT_TEMPLATES_STRUCT();
+ unsigned NumClasses;
+ unsigned NumClassPruners;
+- INT_CLASS_STRUCT *Class[MAX_NUM_CLASSES];
+- CLASS_PRUNER_STRUCT *ClassPruners[MAX_NUM_CLASS_PRUNERS];
++ std::array<INT_CLASS_STRUCT *, MAX_NUM_CLASSES> Class{};
++ std::array<CLASS_PRUNER_STRUCT *, MAX_NUM_CLASS_PRUNERS> ClassPruners{};
+ };
+
+ /* definitions of integer features*/
+diff -rupN --no-dereference tesseract-5.5.3/src/wordrec/tface.cpp tesseract-5.5.3-new/src/wordrec/tface.cpp
+--- tesseract-5.5.3/src/wordrec/tface.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/wordrec/tface.cpp 2026-09-21 10:28:10.440019643 +0200
+@@ -36,14 +36,16 @@ namespace tesseract {
+ * init_permute determines whether to initialize the permute functions
+ * and Dawg models.
+ */
+-void Wordrec::program_editup(const std::string &textbase, TessdataManager *init_classifier,
++bool Wordrec::program_editup(const std::string &textbase, TessdataManager *init_classifier,
+ TessdataManager *init_dict) {
+ if (!textbase.empty()) {
+ imagefile = textbase;
+ }
+ #ifndef DISABLED_LEGACY_ENGINE
+ InitFeatureDefs(&feature_defs_);
+- InitAdaptiveClassifier(init_classifier);
++ if (!InitAdaptiveClassifier(init_classifier)) {
++ return false;
++ }
+ if (init_dict) {
+ getDict().SetupForLoad(Dict::GlobalDawgCache());
+ getDict().Load(lang, init_dict);
+@@ -51,6 +53,7 @@ void Wordrec::program_editup(const std::
+ }
+ pass2_ok_split = chop_ok_split;
+ #endif // ndef DISABLED_LEGACY_ENGINE
++ return true;
+ }
+
+ /**
+diff -rupN --no-dereference tesseract-5.5.3/src/wordrec/wordrec.h tesseract-5.5.3-new/src/wordrec/wordrec.h
+--- tesseract-5.5.3/src/wordrec/wordrec.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/wordrec/wordrec.h 2026-09-21 10:28:10.440242992 +0200
+@@ -50,7 +50,7 @@ public:
+ virtual ~Wordrec() = default;
+
+ // tface.cpp
+- void program_editup(const std::string &textbase, TessdataManager *init_classifier,
++ bool program_editup(const std::string &textbase, TessdataManager *init_classifier,
+ TessdataManager *init_dict);
+ void program_editdown();
+ int end_recog();
+@@ -243,7 +243,7 @@ public:
+ }
+
+ // tface.cpp
+- void program_editup(const std::string &textbase, TessdataManager *init_classifier,
++ bool program_editup(const std::string &textbase, TessdataManager *init_classifier,
+ TessdataManager *init_dict);
+ void cc_recog(WERD_RES *word);
+ void program_editdown();
+diff -rupN --no-dereference tesseract-5.5.3/unittest/CMakeLists.txt tesseract-5.5.3-new/unittest/CMakeLists.txt
+--- tesseract-5.5.3/unittest/CMakeLists.txt 2026-09-21 10:28:10.193362629 +0200
++++ tesseract-5.5.3-new/unittest/CMakeLists.txt 2026-09-21 10:28:10.440480523 +0200
+@@ -58,6 +58,7 @@ set(LEGACY_TESTS
+ equationdetect_test.cc
+ indexmapbidi_test.cc
+ intfeaturemap_test.cc
++ intproto_test.cc
+ mastertrainer_test.cc
+ normproto_test.cc
+ osd_test.cc
+diff -rupN --no-dereference tesseract-5.5.3/unittest/intproto_test.cc tesseract-5.5.3-new/unittest/intproto_test.cc
+--- tesseract-5.5.3/unittest/intproto_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/intproto_test.cc 2026-09-21 10:28:10.440747501 +0200
+@@ -0,0 +1,124 @@
++///////////////////////////////////////////////////////////////////////
++// File: intproto_test.cc
++// Description: Tests that a corrupt TESSDATA_INTTEMP component in a
++// .traineddata file is rejected without memory corruption.
++// The count fields (NumClassPruners, NumClasses,
++// NumProtoSets) are read from the untrusted file and used
++// as loop bounds that write into fixed-size arrays in
++// Classify::ReadIntTemplates, so they must be validated
++// before use.
++//
++// 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 "intproto.h" // for MAX_NUM_CLASS_PRUNERS, MAX_NUM_CLASSES
++#include "tessdatamanager.h"
++
++#include <cstdint>
++#include <cstdio>
++#include <cstdlib>
++#include <string>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// Minimal legacy unicharset component (two characters: 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 an inttemp component in the current (version -5) on-disk layout,
++// as written by Classify::WriteIntTemplates:
++// uint32 unicharset_size, int32 version_id, uint32 NumClassPruners,
++// uint32 NumClasses, then per class: uint16 NumProtos, uint8 NumProtoSets,
++// uint8 NumConfigs.
++std::vector<char> MakeInttemp(int32_t version_id, uint32_t num_class_pruners,
++ uint32_t num_classes, uint32_t unicharset_size,
++ uint8_t num_proto_sets = 0) {
++ std::vector<char> data;
++ auto append = [&data](const void *p, size_t n) {
++ const char *b = static_cast<const char *>(p);
++ data.insert(data.end(), b, b + n);
++ };
++ append(&unicharset_size, sizeof(unicharset_size));
++ append(&version_id, sizeof(version_id));
++ append(&num_class_pruners, sizeof(num_class_pruners));
++ append(&num_classes, sizeof(num_classes));
++ for (uint32_t c = 0; c < num_classes && c < MAX_NUM_CLASSES; ++c) {
++ uint16_t num_protos = 0;
++ uint8_t num_configs = 0;
++ append(&num_protos, sizeof(num_protos));
++ append(&num_proto_sets, sizeof(num_proto_sets));
++ append(&num_configs, sizeof(num_configs));
++ }
++ return data;
++}
++
++// Writes a traineddata file with a minimal unicharset and the given
++// (corrupt) inttemp component to dir/eng.traineddata.
++bool WriteCorruptTraineddata(const std::string &dir, const std::vector<char> &inttemp) {
++ TessdataManager mgr;
++ mgr.OverwriteEntry(TESSDATA_UNICHARSET, kMinUnicharset, sizeof(kMinUnicharset) - 1);
++ mgr.OverwriteEntry(TESSDATA_INTTEMP, inttemp.data(), static_cast<int>(inttemp.size()));
++ return mgr.SaveFile((dir + "/eng.traineddata").c_str(), nullptr);
++}
++
++class IntprotoTest : public testing::Test {
++protected:
++ void SetUp() override {
++ tmpl_ = "/tmp/tess_intproto_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 legacy engine to reject the corrupted traineddata
++ // gracefully (init failure) instead of corrupting memory.
++ void ExpectInitFails(const std::vector<char> &inttemp) {
++ ASSERT_TRUE(WriteCorruptTraineddata(dir_, inttemp));
++ tesseract::TessBaseAPI api;
++ EXPECT_EQ(api.Init(dir_.c_str(), "eng", tesseract::OEM_TESSERACT_ONLY), -1);
++ }
++ std::string dir_;
++ std::string tmpl_;
++};
++
++// NumClassPruners exceeds MAX_NUM_CLASS_PRUNERS: the pruner-read loop would
++// write past the end of INT_TEMPLATES_STRUCT::ClassPruners[].
++TEST_F(IntprotoTest, RejectsTooManyClassPruners) {
++ ExpectInitFails(MakeInttemp(-5, MAX_NUM_CLASS_PRUNERS + 1, 0, 1));
++}
++
++// NumClasses exceeds MAX_NUM_CLASSES: the class-read loop would write past
++// the end of INT_TEMPLATES_STRUCT::Class[].
++TEST_F(IntprotoTest, RejectsTooManyClasses) {
++ ExpectInitFails(MakeInttemp(-5, 0, MAX_NUM_CLASSES + 1, 1));
++}
++
++// A class with NumProtoSets > MAX_NUM_PROTO_SETS: the proto-set loop would
++// write past the end of INT_CLASS_STRUCT::ProtoSets[].
++TEST_F(IntprotoTest, RejectsTooManyProtoSets) {
++ ExpectInitFails(MakeInttemp(-5, 0, 1, 1, MAX_NUM_PROTO_SETS + 1));
++}
++
++// unicharset_size exceeds MAX_NUM_CLASSES: the version < 2 class-id-index
++// read would write past the end of IndexFor[].
++TEST_F(IntprotoTest, RejectsTooLargeUnicharsetSize) {
++ ExpectInitFails(MakeInttemp(-1, 0, 0, MAX_NUM_CLASSES + 1));
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/b494ac18925f9d9aff9ef5815475de9943ab19bf.patch b/b494ac18925f9d9aff9ef5815475de9943ab19bf.patch
new file mode 100644
index 0000000..a7dcd79
--- /dev/null
+++ b/b494ac18925f9d9aff9ef5815475de9943ab19bf.patch
@@ -0,0 +1,258 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.238452893 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.243874130 +0200
+@@ -1171,6 +1171,7 @@ check_PROGRAMS += layout_test
+ check_PROGRAMS += ligature_table_test
+ check_PROGRAMS += linlsq_test
+ check_PROGRAMS += list_test
++check_PROGRAMS += lstm_layer_test
+ if ENABLE_TRAINING
+ check_PROGRAMS += lstm_recode_test
+ check_PROGRAMS += lstm_squashed_test
+@@ -1352,6 +1353,10 @@ loadlang_test_SOURCES = unittest/loadlan
+ loadlang_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ loadlang_test_LDADD = $(TESS_LIBS) $(LEPTONICA_LIBS)
+
++lstm_layer_test_SOURCES = unittest/lstm_layer_test.cc
++lstm_layer_test_CPPFLAGS = $(unittest_CPPFLAGS)
++lstm_layer_test_LDADD = $(TESS_LIBS)
++
+ lstm_recode_test_SOURCES = unittest/lstm_recode_test.cc
+ lstm_recode_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ lstm_recode_test_LDADD = $(TRAINING_LIBS)
+diff -rupN --no-dereference tesseract-5.5.3/src/lstm/lstm.cpp tesseract-5.5.3-new/src/lstm/lstm.cpp
+--- tesseract-5.5.3/src/lstm/lstm.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/lstm/lstm.cpp 2026-09-21 10:28:10.244479752 +0200
+@@ -274,12 +274,32 @@ bool LSTM::DeSerialize(TFile *fp) {
+ is_2d_ = na_ - nf_ == ni_ + 2 * ns_;
+ }
+ }
++ // The deserialized dimensions must be mutually consistent: the forward
++ // pass sizes its buffers from na_, no_ and ns_ while the gate matrices
++ // drive their own dimensions.
++ if (na_ != ni_ + nf_ + (is_2d_ ? 2 : 1) * ns_) {
++ return false;
++ }
++ for (int w = 0; w < WT_COUNT; ++w) {
++ if (w == GFS && !Is2D()) {
++ continue;
++ }
++ if (gate_weights_[w].Dim1() != ns_ || gate_weights_[w].Dim2() != na_ + 1) {
++ return false;
++ }
++ }
++ if ((type_ == NT_LSTM || type_ == NT_LSTM_SUMMARY) && ns_ != no_) {
++ return false;
++ }
+ delete softmax_;
+ if (type_ == NT_LSTM_SOFTMAX || type_ == NT_LSTM_SOFTMAX_ENCODED) {
+ softmax_ = static_cast<FullyConnected *>(Network::CreateFromFile(fp));
+ if (softmax_ == nullptr) {
+ return false;
+ }
++ if (softmax_->NumInputs() != ns_ || softmax_->NumOutputs() != no_) {
++ return false;
++ }
+ } else {
+ softmax_ = nullptr;
+ }
+diff -rupN --no-dereference tesseract-5.5.3/src/lstm/networkio.cpp tesseract-5.5.3-new/src/lstm/networkio.cpp
+--- tesseract-5.5.3/src/lstm/networkio.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/lstm/networkio.cpp 2026-09-21 10:28:10.244595454 +0200
+@@ -641,6 +641,7 @@ void NetworkIO::AddTimeStep(int t, TFloa
+
+ // Adds part of a single timestep to floats.
+ void NetworkIO::AddTimeStepPart(int t, int offset, int num_features, float *inout) const {
++ ASSERT_HOST(offset + num_features <= NumFeatures());
+ if (int_mode_) {
+ const int8_t *line = i_[t] + offset;
+ for (int i = 0; i < num_features; ++i) {
+@@ -662,6 +663,7 @@ void NetworkIO::WriteTimeStep(int t, con
+ // Writes a single timestep from floats in the range [-1, 1] writing only
+ // num_features elements of input to (*this)[t], starting at offset.
+ void NetworkIO::WriteTimeStepPart(int t, int offset, int num_features, const TFloat *input) {
++ ASSERT_HOST(offset + num_features <= NumFeatures());
+ if (int_mode_) {
+ int8_t *line = i_[t] + offset;
+ for (int i = 0; i < num_features; ++i) {
+diff -rupN --no-dereference tesseract-5.5.3/unittest/lstm_layer_test.cc tesseract-5.5.3-new/unittest/lstm_layer_test.cc
+--- tesseract-5.5.3/unittest/lstm_layer_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/lstm_layer_test.cc 2026-09-21 10:28:10.245438307 +0200
+@@ -0,0 +1,177 @@
++///////////////////////////////////////////////////////////////////////
++// File: lstm_layer_test.cc
++// Description: Tests that an NT_LSTM network layer with mutually
++// inconsistent deserialized dimensions is rejected at
++// load. The forward pass sizes its buffers from na_, no_
++// and ns_ while the gate weight matrices drive their own
++// dimensions, so a crafted .traineddata performs heap
++// out-of-bounds writes/reads during the first
++// recognition step (e.g. WriteTimeStepPart writing ns_
++// floats into a source_ buffer sized from na_).
++//
++// 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 "network.h" // for Network, NetworkType
++#include "networkio.h"
++#include "networkscratch.h"
++#include "serialis.h" // for TFile
++#include "stridemap.h"
++
++#include <cstdint>
++#include <utility>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// 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)); }
++ const std::vector<char> &data() const { return data_; }
++
++private:
++ std::vector<char> data_;
++};
++
++void PutDoubleLE(ByteWriter *w, double d) {
++ union {
++ double d;
++ uint64_t u;
++ } conv;
++ conv.d = d;
++ w->PutU32(static_cast<uint32_t>(conv.u & 0xFFFFFFFF));
++ w->PutU32(static_cast<uint32_t>(conv.u >> 32));
++}
++
++// A serialized float-mode WeightMatrix with the given dimensions,
++// all weight data zeroed.
++void PutGateMatrix(ByteWriter *w, int32_t dim1, int32_t dim2) {
++ w->PutU8(128); // mode: kDoubleFlag, float mode
++ w->PutS32(dim1);
++ w->PutS32(dim2);
++ PutDoubleLE(w, 0.0); // empty_ cell
++ for (int32_t i = 0; i < dim1 * dim2; ++i) {
++ PutDoubleLE(w, 0.0);
++ }
++}
++
++// A serialized 1-D NT_LSTM network: header with the given ni/no, na_,
++// then the four gates CI, GI, GF1, GO (GFS is not serialized for 1-D).
++std::vector<char> MakeLstmNetwork(int ni, int no, int32_t na,
++ const int32_t gate_dim1[4], const int32_t gate_dim2[4]) {
++ ByteWriter w;
++ w.PutU8(static_cast<uint32_t>(NT_LSTM));
++ w.PutU8(0); // training: TS_DISABLED
++ w.PutU8(0); // needs_to_backprop
++ w.PutU32(0); // network_flags
++ w.PutU32(static_cast<uint32_t>(ni));
++ w.PutU32(static_cast<uint32_t>(no));
++ w.PutU32(0); // num_weights
++ w.PutU32(0); // name (empty string)
++ w.PutS32(na);
++ for (int g = 0; g < 4; ++g) {
++ PutGateMatrix(&w, gate_dim1[g], gate_dim2[g]);
++ }
++ return w.data();
++}
++
++// Builds the input a standalone LSTM layer would receive: one row of
++// the given width with ni features.
++NetworkIO MakeInput(int ni, int width) {
++ StrideMap stride_map;
++ stride_map.SetStride({{1, width}});
++ NetworkIO input;
++ input.ResizeToMap(false, stride_map, ni);
++ return input;
++}
++
++// Runs Forward on the loaded network; on unpatched code the out-of-
++// bounds access this regression test guards against fires here.
++void RunForward(Network *net, int ni, int width) {
++ NetworkIO input = MakeInput(ni, width);
++ NetworkScratch scratch;
++ NetworkIO output;
++ net->Forward(false, input, nullptr, &scratch, &output);
++ delete net;
++}
++
++// na_ must equal ni_ + nf_ + ns_ for a 1-D LSTM; here na_=2 but the
++// CI matrix makes ns_=64, so the layer must be rejected. On unpatched
++// code Forward writes 64 floats at offset ni_=1 into a source_ buffer
++// sized for na_=2 (heap out-of-bounds write).
++TEST(LstmLayerTest, RejectsInconsistentNa) {
++ const int32_t dim1[4] = {64, 64, 64, 64};
++ const int32_t dim2[4] = {3, 3, 3, 3};
++ std::vector<char> bytes = MakeLstmNetwork(1, 1, 2, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent na_ was accepted";
++}
++
++// All gate matrices must have dim1 == ns_; here the GI matrix has
++// dim1=9 while ns_=5. On unpatched code the GI gate dot product writes
++// 9 results into a temp line sized for 5 (heap out-of-bounds write).
++TEST(LstmLayerTest, RejectsGateDim1Mismatch) {
++ const int32_t dim1[4] = {5, 9, 5, 5};
++ const int32_t dim2[4] = {7, 7, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent gate dim1 was accepted";
++}
++
++// All gate matrices must have dim2 == na_ + 1; here the GI matrix has
++// dim2=9 while na_=6. On unpatched code the GI gate dot product reads
++// 8 inputs from a buffer sized for 6 (heap out-of-bounds read).
++TEST(LstmLayerTest, RejectsGateDim2Mismatch) {
++ const int32_t dim1[4] = {5, 5, 5, 5};
++ const int32_t dim2[4] = {7, 9, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent gate dim2 was accepted";
++}
++
++// A fully consistent 1-D LSTM layer must still be accepted and usable.
++TEST(LstmLayerTest, AcceptsConsistentLayer) {
++ const int32_t dim1[4] = {5, 5, 5, 5};
++ const int32_t dim2[4] = {7, 7, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ ASSERT_NE(net, nullptr);
++ RunForward(net, 1, 2);
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/c94a5532ee04db5a4919542832fd94caee5ea58f.patch b/c94a5532ee04db5a4919542832fd94caee5ea58f.patch
new file mode 100644
index 0000000..d61e30c
--- /dev/null
+++ b/c94a5532ee04db5a4919542832fd94caee5ea58f.patch
@@ -0,0 +1,158 @@
+diff -rupN --no-dereference tesseract-5.5.3/Makefile.am tesseract-5.5.3-new/Makefile.am
+--- tesseract-5.5.3/Makefile.am 2026-09-21 10:28:10.285204667 +0200
++++ tesseract-5.5.3-new/Makefile.am 2026-09-21 10:28:10.291304984 +0200
+@@ -1203,6 +1203,7 @@ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += progress_test
+ check_PROGRAMS += qrsequence_test
+ check_PROGRAMS += recodebeam_test
++check_PROGRAMS += recoder_test
+ check_PROGRAMS += rect_test
+ check_PROGRAMS += resultiterator_test
+ check_PROGRAMS += scanutils_test
+@@ -1439,6 +1440,10 @@ recodebeam_test_SOURCES = unittest/recod
+ recodebeam_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ recodebeam_test_LDADD = $(TRAINING_LIBS) $(ICU_I18N_LIBS) $(ICU_UC_LIBS)
+
++recoder_test_SOURCES = unittest/recoder_test.cc
++recoder_test_CPPFLAGS = $(unittest_CPPFLAGS)
++recoder_test_LDADD = $(TESS_LIBS)
++
+ rect_test_SOURCES = unittest/rect_test.cc
+ rect_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ rect_test_LDADD = $(TESS_LIBS)
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/unicharcompress.cpp tesseract-5.5.3-new/src/ccutil/unicharcompress.cpp
+--- tesseract-5.5.3/src/ccutil/unicharcompress.cpp 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/unicharcompress.cpp 2026-09-21 10:28:10.291889988 +0200
+@@ -400,6 +400,7 @@ void UnicharCompress::SetupDecoder() {
+ for (unsigned c = 0; c < encoder_.size(); ++c) {
+ const RecodedCharID &code = encoder_[c];
+ decoder_[code] = c;
++ ASSERT_HOST(code(0) >= 0 && code(0) < code_range_);
+ is_valid_start_[code(0)] = true;
+ RecodedCharID prefix = code;
+ uint32_t len = code.length();
+diff -rupN --no-dereference tesseract-5.5.3/src/ccutil/unicharcompress.h tesseract-5.5.3-new/src/ccutil/unicharcompress.h
+--- tesseract-5.5.3/src/ccutil/unicharcompress.h 2026-07-24 20:27:02.000000000 +0200
++++ tesseract-5.5.3-new/src/ccutil/unicharcompress.h 2026-09-21 10:28:10.292212478 +0200
+@@ -84,7 +84,17 @@ public:
+ if (length_ > kMaxCodeLen) {
+ return false;
+ }
+- return fp->DeSerialize(&code_[0], length_);
++ if (!fp->DeSerialize(&code_[0], length_)) {
++ return false;
++ }
++ // Code values index arrays sized from the maximum code; reject values
++ // that are out of the sane range for a recoded alphabet.
++ for (uint32_t i = 0; i < length_; ++i) {
++ if (code_[i] < 0 || code_[i] >= static_cast<int32_t>(UINT16_MAX)) {
++ return false;
++ }
++ }
++ return true;
+ }
+ bool operator==(const RecodedCharID &other) const {
+ if (length_ != other.length_) {
+@@ -190,6 +200,7 @@ public:
+ int DecodeUnichar(const RecodedCharID &code) const;
+ // Returns true if the given code is a valid start or single code.
+ bool IsValidFirstCode(int code) const {
++ ASSERT_HOST(code >= 0 && code < code_range_);
+ return is_valid_start_[code];
+ }
+ // Returns a list of valid non-final next codes for a given prefix code,
+diff -rupN --no-dereference tesseract-5.5.3/unittest/recoder_test.cc tesseract-5.5.3-new/unittest/recoder_test.cc
+--- tesseract-5.5.3/unittest/recoder_test.cc 1970-01-01 01:00:00.000000000 +0100
++++ tesseract-5.5.3-new/unittest/recoder_test.cc 2026-09-21 10:28:10.292492242 +0200
+@@ -0,0 +1,91 @@
++///////////////////////////////////////////////////////////////////////
++// File: recoder_test.cc
++// Description: Tests that a UnicharCompress (LSTM recoder) with code
++// values outside the sane range is rejected at load.
++// Negative code values leave code_range_ at zero, so
++// SetupDecoder writes is_valid_start_[code(0)] out of
++// bounds on a size-0 vector<bool>; huge code values wrap
++// code_range_ and make resize() throw.
++//
++// 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 "serialis.h" // for TFile
++#include "unicharcompress.h"
++
++#include <cstdint>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// 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)); }
++ const std::vector<char> &data() const { return data_; }
++
++private:
++ std::vector<char> data_;
++};
++
++// A serialized UnicharCompress with one length-1 RecodedCharID per
++// given code value (self-normalizing).
++std::vector<char> MakeRecoder(const std::vector<int32_t> &codes) {
++ ByteWriter w;
++ w.PutU32(codes.size());
++ for (int32_t code : codes) {
++ w.PutU8(1); // self_normalized_
++ w.PutU32(1); // length_
++ w.PutS32(code); // code_[0]
++ }
++ return w.data();
++}
++
++// A recoder code of -1 keeps code_range_ at 0, so on unpatched code
++// SetupDecoder performs an out-of-bounds write into the size-0
++// is_valid_start_ vector<bool>.
++TEST(RecoderTest, RejectsNegativeCode) {
++ std::vector<char> bytes = MakeRecoder({-1});
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ UnicharCompress recoder;
++ EXPECT_FALSE(recoder.DeSerialize(&fp));
++}
++
++// A recoder code of INT32_MAX wraps code_range_ to a negative value,
++// so on unpatched code SetupDecoder's resize() throws.
++TEST(RecoderTest, RejectsHugeCode) {
++ std::vector<char> bytes = MakeRecoder({INT32_MAX});
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ UnicharCompress recoder;
++ EXPECT_FALSE(recoder.DeSerialize(&fp));
++}
++
++// A valid recoder must still be accepted and usable.
++TEST(RecoderTest, AcceptsValidCodes) {
++ std::vector<char> bytes = MakeRecoder({0, 1});
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ UnicharCompress recoder;
++ ASSERT_TRUE(recoder.DeSerialize(&fp));
++ EXPECT_EQ(recoder.code_range(), 2);
++ EXPECT_TRUE(recoder.IsValidFirstCode(0));
++ EXPECT_TRUE(recoder.IsValidFirstCode(1));
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/tesseract.spec b/tesseract.spec
index cd48d1d..1c83bfa 100644
--- a/tesseract.spec
+++ b/tesseract.spec
@@ -8,7 +8,7 @@
Name: tesseract
Version: 5.5.3
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Raw OCR Engine
License: Apache-2.0
@@ -18,6 +18,22 @@ Source0: https://github.com/tesseract-ocr/tesseract/archive/%{version}%{?p
# Fix library name case
# Build training libs statically
Patch1: tesseract_cmake.patch
+# Backport fix for CVE-2026-88047
+Patch2: https://github.com/tesseract-ocr/tesseract/commit/1bda5079b1c8a7e25f523486837426903d29ce84.patch
+# Backport fix for CVE-2026-88048
+Patch3: https://github.com/tesseract-ocr/tesseract/commit/103dc134eb36411ddc6833ec20aa2c76795bd0ff.patch
+# Backport fix for CVE-2026-88049
+Patch4: https://github.com/tesseract-ocr/tesseract/commit/b494ac18925f9d9aff9ef5815475de9943ab19bf.patch
+# Backport fix for CVE-2026-88050
+Patch5: https://github.com/tesseract-ocr/tesseract/commit/c94a5532ee04db5a4919542832fd94caee5ea58f.patch
+# Backport fix for CVE-2026-88051
+Patch6: https://github.com/tesseract-ocr/tesseract/commit/56e09ca12e751623fe796ce1554ce704bffd2ef0.patch
+# Backport fix for CVE-2026-88052
+Patch7: https://github.com/tesseract-ocr/tesseract/commit/2d04d640db2e8c7e3bab2369d599343b5a8b8443.patch
+# Backport fix for CVE-2026-88053
+Patch8: https://github.com/tesseract-ocr/tesseract/commit/8b0574680f3b22f246ade6a4c8e3029104255c63.patch
+# Backport fix for CVE-2026-88054
+Patch9: https://github.com/tesseract-ocr/tesseract/commit/552771236b0d80cbdb0c7dd856120fa21a4672e5.patch
BuildRequires: cmake
@@ -275,6 +291,9 @@ cp -a doc/*.5 %{buildroot}%{_mandir}/man5/
%changelog
+* Mon Sep 21 2026 Sandro Mani <manisandro@gmail.com> - 5.5.3-2
+- Backport fixes for CVE-2026-{88047-88054}
+
* Wed Jul 29 2026 Sandro Mani <manisandro@gmail.com> - 5.5.3-1
- Update to 5.5.3
diff --git a/tesseract_cmake.patch b/tesseract_cmake.patch
index 64a9194..3ef93ce 100644
--- a/tesseract_cmake.patch
+++ b/tesseract_cmake.patch
@@ -1,6 +1,6 @@
-diff -rupN tesseract-5.5.3/CMakeLists.txt tesseract-5.5.3-new/CMakeLists.txt
+diff -rupN --no-dereference tesseract-5.5.3/CMakeLists.txt tesseract-5.5.3-new/CMakeLists.txt
--- tesseract-5.5.3/CMakeLists.txt 2026-07-24 20:27:02.000000000 +0200
-+++ tesseract-5.5.3-new/CMakeLists.txt 2026-07-29 09:46:19.471456583 +0200
++++ tesseract-5.5.3-new/CMakeLists.txt 2026-09-21 10:28:10.106430166 +0200
@@ -379,7 +379,7 @@ elseif(UNIX)
set(LIB_pthread pthread)
endif()
@@ -46,9 +46,9 @@ diff -rupN tesseract-5.5.3/CMakeLists.txt tesseract-5.5.3-new/CMakeLists.txt
endif()
# ##############################################################################
-diff -rupN tesseract-5.5.3/src/training/CMakeLists.txt tesseract-5.5.3-new/src/training/CMakeLists.txt
+diff -rupN --no-dereference tesseract-5.5.3/src/training/CMakeLists.txt tesseract-5.5.3-new/src/training/CMakeLists.txt
--- tesseract-5.5.3/src/training/CMakeLists.txt 2026-07-24 20:27:02.000000000 +0200
-+++ tesseract-5.5.3-new/src/training/CMakeLists.txt 2026-07-29 09:56:56.137021787 +0200
++++ tesseract-5.5.3-new/src/training/CMakeLists.txt 2026-09-21 10:28:10.106840650 +0200
@@ -107,7 +107,7 @@ if(NOT DISABLED_LEGACY_ENGINE)
common/trainingsampleset.h)
endif()
reply other threads:[~2026-09-21 8:47 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178998043063.1.11241291053047815428.rpms-tesseract-8f15530a402e@fedoraproject.org \
--to=manisandro@gmail.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox