Commit 6b2968d6 for tesseract
commit 6b2968d65cbe62799c2ae316fc00566a07e712c6
Author: Stefan Weil <sw@weilnetz.de>
Date: Fri Nov 13 17:30:50 2026 +0100
classify: reject an inttemp class with too many protos
ReadIntTemplates() allocates ProtoLengths and ProtoSets[] from
NumProtoSets, but the adaption code (FindGoodProtos, AddIntProto)
indexes them by the proto id up to NumProtos. A file with NumProtos >
NumProtoSets * PROTOS_PER_PROTO_SET therefore caused accesses out of
bounds.
Add this condition to the existing check of the counts of a class.
Also avoid a division by a proto length of 0, which can come from a
file, too, in FindGoodProtos() and DisplayProtoDebugInfo().
Add unit tests.
Based on the fix proposed by the Anthropic OSS Scanner
(finding ANT-2026-5F5B0BAI).
Assisted-by: OpenCode / Claude Fable 5.2 (Anthropic)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
diff --git a/src/classify/intmatcher.cpp b/src/classify/intmatcher.cpp
index d4b81c56..d374f146 100644
--- a/src/classify/intmatcher.cpp
+++ b/src/classify/intmatcher.cpp
@@ -580,7 +580,10 @@ int IntegerMatcher::FindGoodProtos(INT_CLASS_STRUCT *ClassTemplate, BIT_VECTOR P
Temp += tables->proto_evidence_[proto][i];
}
- Temp /= ClassTemplate->ProtoLengths[proto];
+ // ConvertProto never stores a length below 1, but a templates file can.
+ if (ClassTemplate->ProtoLengths[proto] > 0) {
+ Temp /= ClassTemplate->ProtoLengths[proto];
+ }
/* Find Good Protos */
if (Temp >= AdaptProtoThreshold) {
@@ -1005,7 +1008,9 @@ void IntegerMatcher::DisplayProtoDebugInfo(INT_CLASS_STRUCT *ClassTemplate, BIT_
temp += tables.proto_evidence_[ActualProtoNum][i];
}
- temp /= ClassTemplate->ProtoLengths[ActualProtoNum];
+ if (ClassTemplate->ProtoLengths[ActualProtoNum] > 0) {
+ temp /= ClassTemplate->ProtoLengths[ActualProtoNum];
+ }
if ((ProtoSet->Protos[ProtoNum]).Configs[0] & (*ConfigMask)) {
DisplayIntProto(ClassTemplate, ActualProtoNum, temp / 255.0);
diff --git a/src/classify/intproto.cpp b/src/classify/intproto.cpp
index 5672de0e..33a29d96 100644
--- a/src/classify/intproto.cpp
+++ b/src/classify/intproto.cpp
@@ -832,9 +832,11 @@ INT_TEMPLATES_STRUCT *Classify::ReadIntTemplates(TFile *fp) {
unsigned num_configs = version_id < 4 ? MaxNumConfigs : Class->NumConfigs;
// 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.
+ // instead of writing out of bounds. ProtoLengths and ProtoSets[] are
+ // sized from NumProtoSets but indexed by proto id up to NumProtos (see
+ // FindGoodProtos and AddIntProto), so the protos must fit in the sets.
if (Class->NumProtos > MAX_NUM_PROTOS || Class->NumProtoSets > MAX_NUM_PROTO_SETS ||
- num_configs > MaxNumConfigs) {
+ Class->NumProtos > MaxNumIntProtosIn(Class) || 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);
diff --git a/unittest/intproto_test.cc b/unittest/intproto_test.cc
index e4d57567..ee5b6e20 100644
--- a/unittest/intproto_test.cc
+++ b/unittest/intproto_test.cc
@@ -6,7 +6,8 @@
// 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.
+// before use. The same goes for the NumProtos of each class,
+// which indexes arrays that are sized from NumProtoSets.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,7 +20,9 @@
#include <tesseract/baseapi.h>
+#include "classify.h" // for Classify (ReadIntTemplates)
#include "intproto.h" // for MAX_NUM_CLASS_PRUNERS, MAX_NUM_CLASSES
+#include "serialis.h" // for TFile
#include "tessdatamanager.h"
#include "util/tempdir.h" // for CreateTempDir, RemoveTempDir
@@ -44,7 +47,7 @@ const char kMinUnicharset[] =
// 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) {
+ uint8_t num_proto_sets = 0, uint16_t num_protos = 0) {
std::vector<char> data;
auto append = [&data](const void *p, size_t n) {
const char *b = static_cast<const char *>(p);
@@ -55,7 +58,6 @@ std::vector<char> MakeInttemp(int32_t version_id, uint32_t num_class_pruners,
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));
@@ -64,6 +66,18 @@ std::vector<char> MakeInttemp(int32_t version_id, uint32_t num_class_pruners,
return data;
}
+// Builds a complete inttemp component in the version 3 on-disk layout (the
+// last one without font tables): no class pruner and one class with the given
+// NumProtos and NumProtoSets, followed by its MAX_NUM_CONFIGS config lengths,
+// its NumProtoSets * PROTOS_PER_PROTO_SET proto lengths and its proto sets,
+// all zero.
+std::vector<char> MakeInttempWithProtos(uint16_t num_protos, uint8_t num_proto_sets) {
+ std::vector<char> data = MakeInttemp(-3, 0, 1, 1, num_proto_sets, num_protos);
+ data.resize(data.size() + MAX_NUM_CONFIGS * sizeof(uint16_t) +
+ num_proto_sets * (PROTOS_PER_PROTO_SET + sizeof(PROTO_SET_STRUCT)));
+ 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
@@ -117,6 +131,17 @@ bool WriteCorruptTraineddata(const std::string &dir, const std::vector<char> &in
return mgr.SaveFile((dir + "/eng.traineddata").c_str(), nullptr);
}
+// Returns whether Classify::ReadIntTemplates accepts the given component.
+bool ReadsInttemp(const std::vector<char> &inttemp) {
+ Classify classifier;
+ TFile fp;
+ fp.Open(inttemp.data(), inttemp.size());
+ INT_TEMPLATES_STRUCT *templates = classifier.ReadIntTemplates(&fp);
+ const bool accepted = templates != nullptr;
+ delete templates;
+ return accepted;
+}
+
class IntprotoTest : public testing::Test {
protected:
void SetUp() override {
@@ -154,6 +179,24 @@ TEST_F(IntprotoTest, RejectsTooManyProtoSets) {
ExpectInitFails(MakeInttemp(-5, 0, 1, 1, MAX_NUM_PROTO_SETS + 1));
}
+// A class with more protos than its proto sets hold: ProtoLengths and
+// ProtoSets[] are sized from NumProtoSets, but the adaption code
+// (IntegerMatcher::FindGoodProtos, AddIntProto) indexes them by proto id up
+// to NumProtos.
+TEST_F(IntprotoTest, RejectsMoreProtosThanProtoSetsHold) {
+ ExpectInitFails(MakeInttemp(-5, 0, 1, 1, 0, 1));
+ ExpectInitFails(MakeInttemp(-5, 0, 1, 1, 1, PROTOS_PER_PROTO_SET + 1));
+ ExpectInitFails(MakeInttemp(-5, 0, 1, 1, 1, MAX_NUM_PROTOS));
+ EXPECT_FALSE(ReadsInttemp(MakeInttempWithProtos(PROTOS_PER_PROTO_SET + 1, 1)));
+}
+
+// A class that uses every place of its proto sets must still be accepted.
+TEST_F(IntprotoTest, AcceptsFullProtoSets) {
+ EXPECT_TRUE(ReadsInttemp(MakeInttempWithProtos(0, 0)));
+ EXPECT_TRUE(ReadsInttemp(MakeInttempWithProtos(PROTOS_PER_PROTO_SET, 1)));
+ EXPECT_TRUE(ReadsInttemp(MakeInttempWithProtos(MAX_NUM_PROTOS, MAX_NUM_PROTO_SETS)));
+}
+
// unicharset_size exceeds MAX_NUM_CLASSES: the version < 2 class-id-index
// read would write past the end of IndexFor[].
TEST_F(IntprotoTest, RejectsTooLargeUnicharsetSize) {