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