new file mode 100644
@@ -0,0 +1,423 @@
+From 9d83107c2511e7e2a7dc649c7bb1633da746669f Mon Sep 17 00:00:00 2001
+From: Stefan Weil <sw@weilnetz.de>
+Date: Sun, 12 Jul 2026 13:49:46 +0200
+Subject: [PATCH] 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>
+(cherry picked from commit 82727cc11c34eaf1249af002d69f6bbae70993b9)
+
+CVE: CVE-2026-73067
+Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/82727cc11c34eaf1249af002d69f6bbae70993b9]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ src/ccstruct/fontinfo.cpp | 10 ++++++---
+ src/ccstruct/fontinfo.h | 2 +-
+ src/ccutil/bitvector.cpp | 4 ++++
+ src/ccutil/serialis.cpp | 10 +++++++++
+ src/ccutil/tessdatamanager.cpp | 9 ++++++++
+ src/ccutil/unicharcompress.h | 19 ++++++++++------
+ src/dict/dawg.cpp | 40 ++++++++++++++++++++++++++--------
+ src/dict/dawg.h | 16 +++++++++-----
+ src/lstm/plumbing.cpp | 4 ++++
+ src/lstm/weightmatrix.cpp | 4 ++++
+ 10 files changed, 92 insertions(+), 26 deletions(-)
+
+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 2e81bbde..67a441e8 100644
+--- a/src/ccutil/unicharcompress.h
++++ b/src/ccutil/unicharcompress.h
+@@ -41,7 +41,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;
+@@ -59,7 +59,7 @@ public:
+ return length_ == 0;
+ }
+ // Accessors
+- int length() const {
++ uint32_t length() const {
+ return length_;
+ }
+ int operator()(int index) const {
+@@ -73,14 +73,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;
+ }
+@@ -91,7 +96,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;
+@@ -103,7 +108,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.
+ int32_t code_[kMaxCodeLen];
+ };
+diff --git a/src/dict/dawg.cpp b/src/dict/dawg.cpp
+index af45176f..dab4dfc2 100644
+--- a/src/dict/dawg.cpp
++++ b/src/dict/dawg.cpp
+@@ -221,7 +221,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
+@@ -234,7 +238,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);
+@@ -274,7 +282,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 {
+@@ -290,7 +302,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);
+@@ -326,14 +342,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_];
+@@ -341,7 +360,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);
+@@ -378,8 +397,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 b87b3880..d0f412c3 100644
+--- a/src/dict/dawg.h
++++ b/src/dict/dawg.h
+@@ -418,7 +418,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),
+@@ -441,7 +441,7 @@ public:
+ return true;
+ }
+
+- int NumEdges() {
++ uint32_t NumEdges() const {
+ return num_edges_;
+ }
+
+@@ -462,7 +462,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
+@@ -516,7 +520,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);
+ }
+ }
+@@ -555,7 +559,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");
+@@ -565,7 +569,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 86255266..d40e9373 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--) {
new file mode 100644
@@ -0,0 +1,96 @@
+From 364a1a76ed19fd2f975f267405f8e36f521e7175 Mon Sep 17 00:00:00 2001
+From: Stefan Weil <sw@weilnetz.de>
+Date: Sun, 12 Jul 2026 18:04:46 +0200
+Subject: [PATCH] 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>
+(cherry picked from commit 55287a94b8044c05ce3fd10f5aca6ebbd238e518)
+
+CVE: CVE-2026-73067
+Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/55287a94b8044c05ce3fd10f5aca6ebbd238e518]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ src/ccutil/serialis.h | 4 ++++
+ src/dict/dawg.cpp | 32 ++++++++++++++++++++++++++++++++
+ 2 files changed, 36 insertions(+)
+
+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 dab4dfc2..75cb9a1f 100644
+--- a/src/dict/dawg.cpp
++++ b/src/dict/dawg.cpp
+@@ -353,12 +353,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_);
@@ -8,6 +8,8 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57"
SRCREV = "6e1d56a847e697de07b38619356550e5cf4e8633"
SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag=${PV} \
file://CVE-2026-73066.patch \
+ file://CVE-2026-73067-1.patch \
+ file://CVE-2026-73067-2.patch \
"