Commit 82727cc1 for tesseract

commit 82727cc11c34eaf1249af002d69f6bbae70993b9
Author: Stefan Weil <sw@weilnetz.de>
Date:   Sun Jul 12 13:49:46 2026 +0200

    Fix memory-safety issues in .traineddata deserialization

    Add bounds checking on count/length fields read from untrusted
    .traineddata files before they are used to size allocations or index
    arrays. Use unsigned types where negative values make no sense.

    Key changes:
    - serialis.cpp: overflow check in DeSerializeSkip, size limits for
      DeSerialize(string) and DeSerialize(vector<char>)
    - unicharcompress.h: validate RecodedCharID length before reading
      into fixed-size code array (buffer overflow fix), change length_
      to uint32_t, use unsigned types for Set() index and length()
    - dawg.cpp/h: use uint32_t for num_edges_, add bounds checks on all
      edge traversal loops, replace no-op ASSERT_HOST with proper errors
    - fontinfo.cpp: bounds check font name size to prevent integer
      overflow in size+1, use uint32_t for spacing vector size
    - tessdatamanager.cpp: validate offsets are within file bounds
    - bitvector.cpp, weightmatrix.cpp, plumbing.cpp: add size limits

    Assisted-by: OpenCode / big-pickle (opencode)
    Signed-off-by: Stefan Weil <stweil@tessus.org>

diff --git a/src/ccstruct/fontinfo.cpp b/src/ccstruct/fontinfo.cpp
index 65d4d032..9984469f 100644
--- a/src/ccstruct/fontinfo.cpp
+++ b/src/ccstruct/fontinfo.cpp
@@ -145,6 +145,10 @@ bool read_info(TFile *f, FontInfo *fi) {
   if (!f->DeSerialize(&size)) {
     return false;
   }
+  // Reject unreasonably large font names to prevent overflow in size + 1.
+  if (size > 100000) {
+    return false;
+  }
   char *font_name = new char[size + 1];
   fi->name = font_name;
   if (!f->DeSerialize(font_name, size)) {
@@ -161,16 +165,16 @@ bool write_info(FILE *f, const FontInfo &fi) {
 }

 bool read_spacing_info(TFile *f, FontInfo *fi) {
-  int32_t vec_size, kern_size;
+  uint32_t vec_size;
+  int32_t kern_size;
   if (!f->DeSerialize(&vec_size)) {
     return false;
   }
-  ASSERT_HOST(vec_size >= 0);
   if (vec_size == 0) {
     return true;
   }
   fi->init_spacing(vec_size);
-  for (int i = 0; i < vec_size; ++i) {
+  for (uint32_t i = 0; i < vec_size; ++i) {
     auto *fs = new FontSpacingInfo();
     if (!f->DeSerialize(&fs->x_gap_before) || !f->DeSerialize(&fs->x_gap_after) ||
         !f->DeSerialize(&kern_size)) {
diff --git a/src/ccstruct/fontinfo.h b/src/ccstruct/fontinfo.h
index 1a84a567..cfb19e06 100644
--- a/src/ccstruct/fontinfo.h
+++ b/src/ccstruct/fontinfo.h
@@ -76,7 +76,7 @@ struct FontInfo {
   bool DeSerialize(TFile *fp);

   // Reserves unicharset_size spots in spacing_vec.
-  void init_spacing(int unicharset_size) {
+  void init_spacing(uint32_t unicharset_size) {
     spacing_vec = new std::vector<FontSpacingInfo *>(unicharset_size);
   }
   // Adds the given pointer to FontSpacingInfo to spacing_vec member
diff --git a/src/ccutil/bitvector.cpp b/src/ccutil/bitvector.cpp
index a02781a8..2b8b7f00 100644
--- a/src/ccutil/bitvector.cpp
+++ b/src/ccutil/bitvector.cpp
@@ -102,6 +102,10 @@ bool BitVector::DeSerialize(bool swap, FILE *fp) {
   if (swap) {
     ReverseN(&new_bit_size, sizeof(new_bit_size));
   }
+  // Reject unreasonably large bit vectors.
+  if (new_bit_size > 500000000) {
+    return false;
+  }
   Alloc(new_bit_size);
   int wordlen = WordLength();
   if (!tesseract::DeSerialize(fp, &array_[0], wordlen)) {
diff --git a/src/ccutil/serialis.cpp b/src/ccutil/serialis.cpp
index d9c9a8d4..cb663094 100644
--- a/src/ccutil/serialis.cpp
+++ b/src/ccutil/serialis.cpp
@@ -88,6 +88,10 @@ bool TFile::DeSerializeSkip(size_t size) {
   if (!DeSerialize(&len)) {
     return false;
   }
+  // Check for overflow: len * size must not overflow size_t.
+  if (size != 0 && len > SIZE_MAX / size) {
+    return false;
+  }
   return Skip(len * size);
 }

@@ -95,6 +99,9 @@ bool TFile::DeSerialize(std::string &data) {
   uint32_t size;
   if (!DeSerialize(&size)) {
     return false;
+  } else if (size > 50000000) {
+    // Arbitrarily limit the size to protect against bad data.
+    return false;
   } else if (size > 0) {
     // TODO: optimize.
     data.resize(size);
@@ -113,6 +120,9 @@ bool TFile::DeSerialize(std::vector<char> &data) {
   uint32_t size;
   if (!DeSerialize(&size)) {
     return false;
+  } else if (size > 50000000) {
+    // Arbitrarily limit the size to protect against bad data.
+    return false;
   } else if (size > 0) {
     // TODO: optimize.
     data.resize(size);
diff --git a/src/ccutil/tessdatamanager.cpp b/src/ccutil/tessdatamanager.cpp
index 8ab26506..67e86dc0 100644
--- a/src/ccutil/tessdatamanager.cpp
+++ b/src/ccutil/tessdatamanager.cpp
@@ -132,14 +132,23 @@ bool TessdataManager::LoadMemBuffer(const char *name, const char *data, int size
   }
   for (unsigned i = 0; i < num_entries && i < TESSDATA_NUM_ENTRIES; ++i) {
     if (offset_table[i] >= 0) {
+      if (offset_table[i] > size) {
+        return false;
+      }
       int64_t entry_size = size - offset_table[i];
       unsigned j = i + 1;
       while (j < num_entries && offset_table[j] == -1) {
         ++j;
       }
       if (j < num_entries) {
+        if (offset_table[j] < 0 || offset_table[j] > size) {
+          return false;
+        }
         entry_size = offset_table[j] - offset_table[i];
       }
+      if (entry_size < 0) {
+        return false;
+      }
       entries_[i].resize(entry_size);
       if (!fp.DeSerialize(&entries_[i][0], entry_size)) {
         return false;
diff --git a/src/ccutil/unicharcompress.h b/src/ccutil/unicharcompress.h
index 16e4e564..f67ae2cf 100644
--- a/src/ccutil/unicharcompress.h
+++ b/src/ccutil/unicharcompress.h
@@ -43,7 +43,7 @@ public:
     length_ = length;
   }
   // Sets the code value at the given index in the code.
-  void Set(int index, int value) {
+  void Set(uint32_t index, int value) {
     code_[index] = value;
     if (length_ <= index) {
       length_ = index + 1;
@@ -61,7 +61,7 @@ public:
     return length_ == 0;
   }
   // Accessors
-  int length() const {
+  uint32_t length() const {
     return length_;
   }
   int operator()(int index) const {
@@ -75,14 +75,19 @@ public:
   }
   // Reads from the given file. Returns false in case of error.
   bool DeSerialize(TFile *fp) {
-    return fp->DeSerialize(&self_normalized_) && fp->DeSerialize(&length_) &&
-           fp->DeSerialize(&code_[0], length_);
+    if (!fp->DeSerialize(&self_normalized_) || !fp->DeSerialize(&length_)) {
+      return false;
+    }
+    if (length_ > kMaxCodeLen) {
+      return false;
+    }
+    return fp->DeSerialize(&code_[0], length_);
   }
   bool operator==(const RecodedCharID &other) const {
     if (length_ != other.length_) {
       return false;
     }
-    for (int i = 0; i < length_; ++i) {
+    for (uint32_t i = 0; i < length_; ++i) {
       if (code_[i] != other.code_[i]) {
         return false;
       }
@@ -93,7 +98,7 @@ public:
   struct RecodedCharIDHash {
     uint64_t operator()(const RecodedCharID &code) const {
       uint64_t result = 0;
-      for (int i = 0; i < code.length_; ++i) {
+      for (uint32_t i = 0; i < code.length_; ++i) {
         result ^= static_cast<uint64_t>(code(i)) << (7 * i);
       }
       return result;
@@ -105,7 +110,7 @@ private:
   // that map to the same code. Has boolean value, but int8_t for serialization.
   int8_t self_normalized_;
   // The number of elements in use in code_;
-  int32_t length_;
+  uint32_t length_;
   // The re-encoded form of the unichar-id to which this RecodedCharID relates.
   std::array<int32_t, kMaxCodeLen> code_;
 };
diff --git a/src/dict/dawg.cpp b/src/dict/dawg.cpp
index c38daa31..15228573 100644
--- a/src/dict/dawg.cpp
+++ b/src/dict/dawg.cpp
@@ -230,7 +230,11 @@ EDGE_REF SquishedDawg::edge_char_of(NODE_REF node, UNICHAR_ID unichar_id,
             (!word_end || end_of_word_from_edge_rec(edges_[edge]))) {
           return (edge);
         }
-      } while (!last_edge(edge++));
+        if (last_edge(edge)) {
+          break;
+        }
+        ++edge;
+      } while (edge < num_edges_);
     }
   }
   return (NO_EDGE); // not found
@@ -243,7 +247,11 @@ int32_t SquishedDawg::num_forward_edges(NODE_REF node) const {
   if (forward_edge(edge)) {
     do {
       num++;
-    } while (!last_edge(edge++));
+      if (last_edge(edge)) {
+        break;
+      }
+      ++edge;
+    } while (edge < num_edges_);
   }

   return (num);
@@ -283,7 +291,11 @@ void SquishedDawg::print_node(NODE_REF node, int max_num_edges) const {
       if (edge - node > max_num_edges) {
         return;
       }
-    } while (!last_edge(edge++));
+      if (last_edge(edge)) {
+        break;
+      }
+      ++edge;
+    } while (edge < num_edges_);

     if (edge < num_edges_ && edge_occupied(edge) && backward_edge(edge)) {
       do {
@@ -299,7 +311,11 @@ void SquishedDawg::print_node(NODE_REF node, int max_num_edges) const {
         if (edge - node > MAX_NODE_EDGES_DISPLAY) {
           return;
         }
-      } while (!last_edge(edge++));
+        if (last_edge(edge)) {
+          break;
+        }
+        ++edge;
+      } while (edge < num_edges_);
     }
   } else {
     tprintf(REFFORMAT " : no edges in this node\n", node);
@@ -335,14 +351,17 @@ bool SquishedDawg::read_squished_dawg(TFile *file) {
     return false;
   }

-  int32_t unicharset_size;
+  uint32_t unicharset_size;
   if (!file->DeSerialize(&unicharset_size)) {
     return false;
   }
   if (!file->DeSerialize(&num_edges_)) {
     return false;
   }
-  ASSERT_HOST(num_edges_ > 0); // DAWG should not be empty
+  if (num_edges_ == 0) {
+    tprintf("Empty dawg: num_edges is 0\n");
+    return false;
+  }
   Dawg::init(unicharset_size);

   edges_ = new EDGE_RECORD[num_edges_];
@@ -350,7 +369,7 @@ bool SquishedDawg::read_squished_dawg(TFile *file) {
     return false;
   }
   if (debug_level_ > 2) {
-    tprintf("type: %d lang: %s perm: %d unicharset_size: %d num_edges: %d\n",
+    tprintf("type: %d lang: %s perm: %d unicharset_size: %d num_edges: %" PRIu32 "\n",
             type_, lang_.c_str(), perm_, unicharset_size_, num_edges_);
     for (EDGE_REF edge = 0; edge < num_edges_; ++edge) {
       print_edge(edge);
@@ -387,8 +406,11 @@ std::unique_ptr<EDGE_REF[]> SquishedDawg::build_node_map(
         break;
       }
       if (backward_edge(edge)) {
-        while (!last_edge(edge++)) {
-          ;
+        while (edge < num_edges_ && !last_edge(edge)) {
+          ++edge;
+        }
+        if (edge < num_edges_) {
+          ++edge; // Skip past the last backward edge.
         }
       }
       edge--;
diff --git a/src/dict/dawg.h b/src/dict/dawg.h
index 08d737e4..87bfc0e3 100644
--- a/src/dict/dawg.h
+++ b/src/dict/dawg.h
@@ -413,7 +413,7 @@ public:
     ASSERT_HOST(read_squished_dawg(&file));
     num_forward_edges_in_node0 = num_forward_edges(0);
   }
-  SquishedDawg(EDGE_ARRAY edges, int num_edges, DawgType type,
+  SquishedDawg(EDGE_ARRAY edges, uint32_t num_edges, DawgType type,
                const std::string &lang, PermuterType perm, int unicharset_size,
                int debug_level)
       : Dawg(type, lang, perm, debug_level),
@@ -436,7 +436,7 @@ public:
     return true;
   }

-  int NumEdges() {
+  uint32_t NumEdges() const {
     return num_edges_;
   }

@@ -457,7 +457,11 @@ public:
       if (!word_end || end_of_word_from_edge_rec(edges_[edge])) {
         vec->push_back(NodeChild(unichar_id_from_edge_rec(edges_[edge]), edge));
       }
-    } while (!last_edge(edge++));
+      if (last_edge(edge)) {
+        break;
+      }
+      ++edge;
+    } while (edge < num_edges_);
   }

   /// Returns the next node visited by following the edge
@@ -511,7 +515,7 @@ private:
   }
   /// Goes through all the edges and clears each one out.
   inline void clear_all_edges() {
-    for (int edge = 0; edge < num_edges_; edge++) {
+    for (uint32_t edge = 0; edge < num_edges_; edge++) {
       set_empty_edge(edge);
     }
   }
@@ -550,7 +554,7 @@ private:
   /// Prints the contents of the SquishedDawg.
   void print_all(const char *msg) {
     tprintf("\n__________________________\n%s\n", msg);
-    for (int i = 0; i < num_edges_; ++i) {
+    for (uint32_t i = 0; i < num_edges_; ++i) {
       print_edge(i);
     }
     tprintf("__________________________\n");
@@ -560,7 +564,7 @@ private:

   // Member variables.
   EDGE_ARRAY edges_ = nullptr;
-  int32_t num_edges_ = 0;
+  uint32_t num_edges_ = 0;
   int num_forward_edges_in_node0 = 0;
 };

diff --git a/src/lstm/plumbing.cpp b/src/lstm/plumbing.cpp
index ebb6612e..f0133148 100644
--- a/src/lstm/plumbing.cpp
+++ b/src/lstm/plumbing.cpp
@@ -222,6 +222,10 @@ bool Plumbing::DeSerialize(TFile *fp) {
   if (!fp->DeSerialize(&size)) {
     return false;
   }
+  // Reject unreasonably large network stacks.
+  if (size > 10000) {
+    return false;
+  }
   for (uint32_t i = 0; i < size; ++i) {
     Network *network = CreateFromFile(fp);
     if (network == nullptr) {
diff --git a/src/lstm/weightmatrix.cpp b/src/lstm/weightmatrix.cpp
index d3ca2fcc..ef21daef 100644
--- a/src/lstm/weightmatrix.cpp
+++ b/src/lstm/weightmatrix.cpp
@@ -295,6 +295,10 @@ bool WeightMatrix::DeSerialize(bool training, TFile *fp) {
     if (!fp->DeSerialize(&size)) {
       return false;
     }
+    // Reject unreasonably large scale vectors.
+    if (size > 100000000) {
+      return false;
+    }
 #ifdef FAST_FLOAT
     scales_.reserve(size);
     for (auto n = size; n > 0; n--) {