Commit 8b057468 for tesseract
commit 8b0574680f3b22f246ade6a4c8e3029104255c63
Author: Stefan Weil <sw@weilnetz.de>
Date: Fri Aug 21 13:47:58 2026 +0200
Fix out-of-bounds writes in .traineddata inttemp deserialization
Classify::ReadIntTemplates reads NumClassPruners, NumClasses and
NumProtoSets from the untrusted inttemp component of a .traineddata
file and used them as loop bounds writing pointers into fixed-size
arrays (ClassPruners[], Class[], ProtoSets[]) without validation. A
crafted file with counts larger than the array capacity caused heap
out-of-bounds pointer writes during legacy engine initialization,
before any OCR is performed.
Key changes:
- intproto.cpp: validate unicharset_size, NumClassPruners, NumClasses,
per-class NumProtos/NumProtoSets/NumConfigs and the old-format
(version < 2) class ids against the array capacities and fail the
load instead of writing out of bounds. Never trust file-sourced
counts to bound the destructor loops.
- adaptive.cpp/h: same treatment in ReadAdaptedTemplates and
ReadAdaptedClass (NumTempProtos, NumConfigs); the
ADAPT_TEMPLATES_STRUCT default constructor now initializes all
members and the destructor is null-safe.
- adaptmatch.cpp, tface.cpp, tessedit.cpp: propagate the read failure
through InitAdaptiveClassifier and program_editup so the language
load fails gracefully instead of continuing with a broken state.
- intproto.h, adaptive.h: replace the fixed-size member arrays by
std::array (identical on-disk layout, value-initialized).
- unittest: add intproto_test, which builds a minimal traineddata
with corrupt inttemp counts and expects a graceful init failure.
On unpatched code the test trips ASan on the original
heap-buffer-overflow in ReadIntTemplates.
Reported-by: Zhixi "Jace" Sun <g.mygenie@gmail.com>
Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
diff --git a/Makefile.am b/Makefile.am
index 5df35d6f..08925be0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1163,6 +1163,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
@@ -1310,6 +1311,12 @@ intfeaturemap_test_CPPFLAGS = $(unittest_CPPFLAGS)
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 --git a/src/ccmain/tessedit.cpp b/src/ccmain/tessedit.cpp
index 412b8c15..cfbeca62 100644
--- a/src/ccmain/tessedit.cpp
+++ b/src/ccmain/tessedit.cpp
@@ -418,7 +418,9 @@ int Tesseract::init_tesseract_internal(const std::string &textbase,
// 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 --git a/src/classify/adaptive.cpp b/src/classify/adaptive.cpp
index e3297c0e..96850ab7 100644
--- a/src/classify/adaptive.cpp
+++ b/src/classify/adaptive.cpp
@@ -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(FILE *File, ADAPT_TEMPLATES_STRUCT *Templat
* @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(TFile *fp) {
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(TFile *fp) {
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 --git a/src/classify/adaptive.h b/src/classify/adaptive.h
index 652e1fbd..fde3ef22 100644
--- a/src/classify/adaptive.h
+++ b/src/classify/adaptive.h
@@ -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 --git a/src/classify/adaptmatch.cpp b/src/classify/adaptmatch.cpp
index f9f38fbe..30d30a81 100644
--- a/src/classify/adaptmatch.cpp
+++ b/src/classify/adaptmatch.cpp
@@ -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(TessdataManager *mgr) {
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(TessdataManager *mgr) {
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() {
@@ -1243,8 +1254,8 @@ UNICHAR_ID *Classify::BaselineClassifier(TBLOB *Blob,
}
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 --git a/src/classify/classify.h b/src/classify/classify.h
index 1a511c28..8ebf451f 100644
--- a/src/classify/classify.h
+++ b/src/classify/classify.h
@@ -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 --git a/src/classify/intproto.cpp b/src/classify/intproto.cpp
index b199ae28..15cd260d 100644
--- a/src/classify/intproto.cpp
+++ b/src/classify/intproto.cpp
@@ -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,41 +582,36 @@ INT_CLASS_STRUCT::INT_CLASS_STRUCT(int MaxNumProtos) :
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;
- }
- for (int i = 0; i < MAX_NUM_CLASS_PRUNERS; i++) {
- ClassPruners[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];
}
}
@@ -669,6 +663,20 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
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;
@@ -708,6 +716,16 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
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));
@@ -777,8 +795,20 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
}
}
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) {
@@ -812,8 +842,9 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
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 --git a/src/classify/intproto.h b/src/classify/intproto.h
index 5365bee3..d1a5a20d 100644
--- a/src/classify/intproto.h
+++ b/src/classify/intproto.h
@@ -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 --git a/src/wordrec/tface.cpp b/src/wordrec/tface.cpp
index c058d4ab..146f0b01 100644
--- a/src/wordrec/tface.cpp
+++ b/src/wordrec/tface.cpp
@@ -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::string &textbase, TessdataManager *init_
}
pass2_ok_split = chop_ok_split;
#endif // ndef DISABLED_LEGACY_ENGINE
+ return true;
}
/**
diff --git a/src/wordrec/wordrec.h b/src/wordrec/wordrec.h
index a50ad871..ecf67f02 100644
--- a/src/wordrec/wordrec.h
+++ b/src/wordrec/wordrec.h
@@ -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 --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
index a660c9b1..c5294955 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -58,6 +58,7 @@ set(LEGACY_TESTS
equationdetect_test.cc
indexmapbidi_test.cc
intfeaturemap_test.cc
+ intproto_test.cc
mastertrainer_test.cc
osd_test.cc
params_model_test.cc
diff --git a/unittest/intproto_test.cc b/unittest/intproto_test.cc
new file mode 100644
index 00000000..ea0c602e
--- /dev/null
+++ b/unittest/intproto_test.cc
@@ -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