Commit 76865fa0 for tesseract
commit 76865fa087c15f5ea2f75f697993fc3b05eabaf2
Author: Stefan Weil <sw@weilnetz.de>
Date: Fri Aug 21 14:52:22 2026 +0200
Make UNICHARSET::has_special_codes bounds-safe
UNICHARSET::has_special_codes called get_fragment(UNICHAR_BROKEN)
without checking that the id exists, so a .traineddata whose LSTM
unicharset has fewer than three entries aborted the process during
initialization via LSTMRecognizer::LoadRecoder ->
UnicharCompress::SetupPassThrough (deterministic denial of service).
Older traineddata files may legitimately lack the special codes
(e.g. models predating them); SetupPassThrough and the fragment
handling cope with has_special_codes() == false, so such models
must still load.
Key changes:
- unicharset.h: make has_special_codes bounds-safe; it now returns
false instead of aborting when the Broken special code is absent.
- unittest: extend plumbing_test with a valid network plus a
two-entry unicharset without the special codes; the model must
load cleanly (on unpatched code the test dies on the original
assert in UNICHARSET::get_fragment).
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/ccutil/unicharset.h b/src/ccutil/unicharset.h
index a37ad5cf..69b085ff 100644
--- a/src/ccutil/unicharset.h
+++ b/src/ccutil/unicharset.h
@@ -754,7 +754,8 @@ public:
// SpecialUnicharCodes available. If false then there are normal unichars
// at these codes and they should not be used.
bool has_special_codes() const {
- return get_fragment(UNICHAR_BROKEN) != nullptr &&
+ return contains_unichar_id(UNICHAR_BROKEN) &&
+ get_fragment(UNICHAR_BROKEN) != nullptr &&
strcmp(id_to_unichar(UNICHAR_BROKEN),
kSpecialUnicharCodes[UNICHAR_BROKEN]) == 0;
}
diff --git a/unittest/plumbing_test.cc b/unittest/plumbing_test.cc
index 27486f86..53e1642b 100644
--- a/unittest/plumbing_test.cc
+++ b/unittest/plumbing_test.cc
@@ -40,6 +40,13 @@ const char kMinUnicharset[] =
"Joined 7 0,69,188,255,486,1218,0,30,486,1188 Latin 26 0 98 Joined\n"
"|Broken|0|1 f 0,69,186,255,892,2138,0,80,892,2058 Common 84 10 84 |Broken|0|1\n";
+// A unicharset without the special codes (Broken in particular), as in
+// older models.
+const char kSmallUnicharset[] =
+ "2\n"
+ "NULL 0 NULL 0\n"
+ "a 1 0,255,0,255,0,0,0,0,0,0 Latin 2 0 2\n";
+
// Appends raw little-endian values to a byte buffer.
class ByteWriter {
public:
@@ -55,6 +62,7 @@ public:
data_.insert(data_.end(), s, s + std::strlen(s));
}
void PutRaw(const char *s) { data_.insert(data_.end(), s, s + std::strlen(s)); }
+ void PutBytes(const std::vector<char> &b) { data_.insert(data_.end(), b.begin(), b.end()); }
const std::vector<char> &data() const { return data_; }
private:
@@ -75,8 +83,8 @@ void AppendNetworkHeader(ByteWriter *w, NetworkType type) {
w->PutString(""); // name
}
-// A minimal valid child network (NT_INPUT with a 1x1x1x1 shape).
-void AppendInputChild(ByteWriter *w) {
+// A minimal valid NT_INPUT network (header plus a 1x1x1x1 shape).
+void AppendInputNetwork(ByteWriter *w) {
AppendNetworkHeader(w, NT_INPUT);
w->PutS32(1); // batch
w->PutS32(1); // height
@@ -85,25 +93,41 @@ void AppendInputChild(ByteWriter *w) {
w->PutS32(0); // loss type
}
-// Builds a TESSDATA_LSTM component whose top-level network is a plumbing
-// layer of the given type with the given (corrupt) stack size, followed by
-// the remaining fields of LSTMRecognizer::DeSerialize.
-std::vector<char> MakeLstmComponent(NetworkType type, uint32_t stack_size) {
+// Builds a serialized top-level network: a plumbing layer of the given
+// type with the given (corrupt) stack size of NT_INPUT children.
+std::vector<char> MakePlumbingNetwork(NetworkType type, uint32_t stack_size) {
ByteWriter w;
AppendNetworkHeader(&w, type);
w.PutU32(stack_size); // Plumbing::DeSerialize reads this as uint32
for (uint32_t i = 0; i < stack_size; ++i) {
- AppendInputChild(&w);
+ AppendInputNetwork(&w);
}
- w.PutRaw(kMinUnicharset); // unicharset (raw text, no recoder/unicharset components)
- w.PutString(""); // network_str_
- w.PutS32(0); // training_flags_
- w.PutS32(0); // training_iteration_
- w.PutS32(0); // sample_iteration_
- w.PutS32(0); // null_char_
- w.PutU32(0); // adam_beta_ (float 0.0)
- w.PutU32(0); // learning_rate_ (float 0.0)
- w.PutU32(0); // momentum_ (float 0.0)
+ return w.data();
+}
+
+// Builds a serialized top-level NT_INPUT network.
+std::vector<char> MakeInputNetwork() {
+ ByteWriter w;
+ AppendInputNetwork(&w);
+ return w.data();
+}
+
+// Builds a TESSDATA_LSTM component with the given serialized network and
+// the given (corrupt) unicharset, followed by the remaining fields of
+// LSTMRecognizer::DeSerialize.
+std::vector<char> MakeLstmComponent(const std::vector<char> &network,
+ const char *unicharset) {
+ ByteWriter w;
+ w.PutBytes(network);
+ w.PutRaw(unicharset); // unicharset (raw text, no recoder/unicharset components)
+ w.PutString(""); // network_str_
+ w.PutS32(0); // training_flags_
+ w.PutS32(0); // training_iteration_
+ w.PutS32(0); // sample_iteration_
+ w.PutS32(0); // null_char_
+ w.PutU32(0); // adam_beta_ (float 0.0)
+ w.PutU32(0); // learning_rate_ (float 0.0)
+ w.PutU32(0); // momentum_ (float 0.0)
return w.data();
}
@@ -141,24 +165,34 @@ protected:
// Empty NT_SERIES stack: Series::CacheXScaleFactor would dereference
// stack_[0] on the empty vector during initialization.
TEST_F(PlumbingTest, RejectsEmptySeriesStack) {
- ExpectInitFails(MakeLstmComponent(NT_SERIES, 0));
+ ExpectInitFails(MakeLstmComponent(MakePlumbingNetwork(NT_SERIES, 0), kMinUnicharset));
}
// Empty NT_PARALLEL stack: Plumbing::XScaleFactor would dereference
// stack_[0] on the empty vector during initialization.
TEST_F(PlumbingTest, RejectsEmptyParallelStack) {
- ExpectInitFails(MakeLstmComponent(NT_PARALLEL, 0));
+ ExpectInitFails(MakeLstmComponent(MakePlumbingNetwork(NT_PARALLEL, 0), kMinUnicharset));
}
// Empty NT_XREVERSED stack: same crash as the parallel case.
TEST_F(PlumbingTest, RejectsEmptyReversedStack) {
- ExpectInitFails(MakeLstmComponent(NT_XREVERSED, 0));
+ ExpectInitFails(MakeLstmComponent(MakePlumbingNetwork(NT_XREVERSED, 0), kMinUnicharset));
}
// A Series with a single network: Series::Forward requires at least two
// networks, so such a model can never work.
TEST_F(PlumbingTest, RejectsSingleNetworkSeries) {
- ExpectInitFails(MakeLstmComponent(NT_SERIES, 1));
+ ExpectInitFails(MakeLstmComponent(MakePlumbingNetwork(NT_SERIES, 1), kMinUnicharset));
+}
+
+// A valid network but a unicharset without the special codes, as in
+// older models: has_special_codes used to abort on the missing Broken
+// special code; the model must now load cleanly.
+TEST_F(PlumbingTest, ToleratesUnicharsetWithoutSpecialCodes) {
+ ASSERT_TRUE(WriteCorruptTraineddata(
+ dir_, MakeLstmComponent(MakeInputNetwork(), kSmallUnicharset)));
+ tesseract::TessBaseAPI api;
+ EXPECT_EQ(api.Init(dir_.c_str(), "eng", tesseract::OEM_LSTM_ONLY), 0);
}
} // namespace