tesseract: patch CVE-2026-73067

Details:
https://nvd.nist.gov/vuln/detail/cve-2026-73067

Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Ankur Tyagi
2026-09-15 10:13:36 +05:30
committed by Anuj Mittal
parent 10f315bace
commit e8277376bf
3 changed files with 521 additions and 0 deletions
@@ -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--) {
@@ -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 \
"