Commit 55287a94 for tesseract

commit 55287a94b8044c05ce3fd10f5aca6ebbd238e518
Author: Stefan Weil <sw@weilnetz.de>
Date:   Sun Jul 12 18:04:46 2026 +0200

    Fix unbounded allocation and validate DAWG edge structure

    Bound num_edges_ against remaining component bytes before allocating
    the edges_ array in read_squished_dawg. This prevents a crafted
    num_edges_ = 0x7FFFFFFF from requesting ~17 GB and aborting via
    uncaught std::bad_alloc.

    After loading, validate the edge structure:
    - Reject edges whose next_node value exceeds num_edges_ (wild-index
      read during dictionary lookup).
    - Reject unterminated forward edge runs (the original heap OOB read).

    Also add TFile::RemainingBytes() to query how many bytes are left to
    read from the current position.

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

diff --git a/src/ccutil/serialis.h b/src/ccutil/serialis.h
index d6e4a996..59a4e1f1 100644
--- a/src/ccutil/serialis.h
+++ b/src/ccutil/serialis.h
@@ -75,6 +75,10 @@ public:
   void set_swap(bool value) {
     swap_ = value;
   }
+  // Returns the number of bytes remaining to be read.
+  size_t RemainingBytes() const {
+    return data_ != nullptr && offset_ < data_->size() ? data_->size() - offset_ : 0;
+  }

   // Deserialize data.
   bool DeSerializeSize(int32_t *data);
diff --git a/src/dict/dawg.cpp b/src/dict/dawg.cpp
index 15228573..d72899c0 100644
--- a/src/dict/dawg.cpp
+++ b/src/dict/dawg.cpp
@@ -362,12 +362,44 @@ bool SquishedDawg::read_squished_dawg(TFile *file) {
     tprintf("Empty dawg: num_edges is 0\n");
     return false;
   }
+  // Reject if the declared edge count exceeds the remaining component bytes.
+  if (num_edges_ > file->RemainingBytes() / sizeof(EDGE_RECORD)) {
+    tprintf("Dawg num_edges %u exceeds remaining data\n", num_edges_);
+    return false;
+  }
   Dawg::init(unicharset_size);

   edges_ = new EDGE_RECORD[num_edges_];
   if (!file->DeSerialize(&edges_[0], num_edges_)) {
     return false;
   }
+  // Validate the loaded edge structure: check that next_node values are in
+  // bounds and that forward edge runs are properly terminated.
+  for (uint32_t i = 0; i < num_edges_; ++i) {
+    if (edges_[i] == next_node_mask_) {
+      continue; // Empty slot.
+    }
+    NODE_REF next = next_node_from_edge_rec(edges_[i]);
+    if (next != 0 && static_cast<uint32_t>(next) >= num_edges_) {
+      tprintf("Dawg edge %u has out-of-bounds next_node\n", i);
+      return false;
+    }
+    if (forward_edge(i)) {
+      uint32_t j = i;
+      bool terminated = false;
+      do {
+        if (last_edge(j)) {
+          terminated = true;
+          break;
+        }
+        ++j;
+      } while (j < num_edges_);
+      if (!terminated) {
+        tprintf("Dawg forward edge run starting at %u is not terminated\n", i);
+        return false;
+      }
+    }
+  }
   if (debug_level_ > 2) {
     tprintf("type: %d lang: %s perm: %d unicharset_size: %d num_edges: %" PRIu32 "\n",
             type_, lang_.c_str(), perm_, unicharset_size_, num_edges_);