Commit d310b6d6 for tesseract
commit d310b6d648a3d71795ddb0f4c918e4bfd01d6d9f
Author: Stefan Weil <sw@weilnetz.de>
Date: Fri Oct 9 05:15:16 2026 +0200
Reject negative/oversized class ids in legacy inttemp (#4640)
The version < 2 path of ReadIntTemplates reads a signed ClassIdFor[]
table from the file. Each id indexes Class[] and (divided by
CLASSES_PER_CP) ClassPruners[]. The existing guard only bounds the
maximum id, but the max is initialised to 0 and grows only on
ClassIdFor[i] > max_class_id, so a negative id never raises it and slips
through to the raw std::array::operator[] sinks (an OOB pointer write at
Class[negative] and an OOB pruner read/write through the converted bits).
Validate every id individually with LegalClassId (c >= 0 && c <
MAX_NUM_CLASSES) right after the table is read, before any pruner is
allocated, so a corrupt or malicious file is rejected cleanly. This is
independent of the later upper-bound check, which is kept for the
positive ids it covers.
Add legacy version-1 inttemp tests: a negative class id and a class id at
MAX_NUM_CLASSES are both rejected at init time. Under AddressSanitizer
the unfixed code aborts after the out-of-bounds write; the fixed build
fails init cleanly. The version-1 fixture writes exactly one class
pruner (WERDS_PER_CP words), derived from the format constants, so the
class records sit at their documented on-disk offset.
Fixes GHSA-96jm-jc65-f968
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/src/classify/intproto.cpp b/src/classify/intproto.cpp
index 15cd260d..36d117be 100644
--- a/src/classify/intproto.cpp
+++ b/src/classify/intproto.cpp
@@ -691,6 +691,17 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
Templates->NumClasses) {
tprintf("Bad read of inttemp!\n");
}
+ // Each class id indexes Class[] and (divided by CLASSES_PER_CP)
+ // ClassPruners[]. A negative id does not raise max_class_id and therefore
+ // evades the upper-bound check below. Validate every id, including oversized
+ // positive values, before any class pruner is allocated or the id is used.
+ for (unsigned i = 0; i < Templates->NumClasses; i++) {
+ if (!LegalClassId(ClassIdFor[i])) {
+ tprintf("Error: illegal class id %d in inttemp\n", ClassIdFor[i]);
+ delete Templates;
+ return nullptr;
+ }
+ }
}
/* then read in the class pruners */
diff --git a/unittest/intproto_test.cc b/unittest/intproto_test.cc
index ea0c602e..dc8110d1 100644
--- a/unittest/intproto_test.cc
+++ b/unittest/intproto_test.cc
@@ -64,6 +64,50 @@ std::vector<char> MakeInttemp(int32_t version_id, uint32_t num_class_pruners,
return data;
}
+// Builds an inttemp component in the old (version 1) on-disk layout. Besides
+// the common header this carries the IndexFor[] table (unicharset_size x
+// int16), the ClassIdFor[] table (num_classes x int32) and one class pruner
+// (NUM_CP_BUCKETS^3 x WERDS_PER_CP_VECTOR uint32 words), as read by the version < 2 path of
+// Classify::ReadIntTemplates.
+std::vector<char> MakeLegacyInttemp(int32_t version_id, uint32_t num_classes,
+ uint32_t unicharset_size,
+ const std::vector<int32_t> &class_ids) {
+ 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);
+ };
+ uint32_t num_class_pruners = 1;
+ 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 i = 0; i < unicharset_size; ++i) {
+ int16_t index = 0;
+ append(&index, sizeof(index));
+ }
+ for (uint32_t i = 0; i < num_classes; ++i) {
+ int32_t class_id =
+ i < class_ids.size() ? class_ids[i] : static_cast<int32_t>(i);
+ append(&class_id, sizeof(class_id));
+ }
+ // One class pruner is NUM_CP_BUCKETS^3 vectors of WERDS_PER_CP_VECTOR words
+ // each, i.e. WERDS_PER_CP uint32 words total.
+ for (uint32_t w = 0; w < WERDS_PER_CP; ++w) {
+ uint32_t zero = 0;
+ append(&zero, sizeof(zero));
+ }
+ uint16_t num_protos = 0;
+ uint8_t num_proto_sets = 0;
+ uint8_t num_configs = 0;
+ for (uint32_t c = 0; c < num_classes && c < MAX_NUM_CLASSES; ++c) {
+ 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) {
@@ -120,5 +164,19 @@ TEST_F(IntprotoTest, RejectsTooLargeUnicharsetSize) {
ExpectInitFails(MakeInttemp(-1, 0, 0, MAX_NUM_CLASSES + 1));
}
+// A version 1 inttemp whose ClassIdFor[] carries a negative class id. The
+// max_class_id upper-bound check can never fire for a negative id, so it
+// must be rejected by the individual LegalClassId check before it indexes
+// Class[].
+TEST_F(IntprotoTest, RejectsNegativeClassId) {
+ ExpectInitFails(MakeLegacyInttemp(-1, 1, 2, {static_cast<int32_t>(-30000)}));
+}
+
+// Same, with a class id at exactly MAX_NUM_CLASSES: above the legal range,
+// but still a positive value.
+TEST_F(IntprotoTest, RejectsTooLargeClassId) {
+ ExpectInitFails(MakeLegacyInttemp(-1, 1, 2, {MAX_NUM_CLASSES}));
+}
+
} // namespace
} // namespace tesseract