Commit c21194d0 for tesseract
commit c21194d06998e244754d65329d6c9f1e14fe392e
Author: m00dy <professor.moody@pm.me>
Date: Mon Nov 2 12:20:18 2026 -0600
Validate the LSTM softmax child runtime type during deserialization (#4670)
Network::CreateFromFile can return a network subclass other than
FullyConnected. Verify the runtime type before storing the child, delete
rejected networks, and leave softmax_ null on failure.
Add tests for rejected Input children and valid FullyConnected children
across both softmax LSTM variants.
Assisted-by: AI tooling (static audit and test drafting)
Signed-off-by: Nathan Keys <professor.moody@pm.me>
diff --git a/src/lstm/lstm.cpp b/src/lstm/lstm.cpp
index bfffa5de..4165d154 100644
--- a/src/lstm/lstm.cpp
+++ b/src/lstm/lstm.cpp
@@ -292,16 +292,16 @@ bool LSTM::DeSerialize(TFile *fp) {
return false;
}
delete softmax_;
+ softmax_ = nullptr;
if (type_ == NT_LSTM_SOFTMAX || type_ == NT_LSTM_SOFTMAX_ENCODED) {
- softmax_ = static_cast<FullyConnected *>(Network::CreateFromFile(fp));
- if (softmax_ == nullptr) {
+ Network *network = Network::CreateFromFile(fp);
+ auto *softmax = dynamic_cast<FullyConnected *>(network);
+ if (softmax == nullptr || softmax->NumInputs() != ns_ ||
+ softmax->NumOutputs() != no_) {
+ delete network;
return false;
}
- if (softmax_->NumInputs() != ns_ || softmax_->NumOutputs() != no_) {
- return false;
- }
- } else {
- softmax_ = nullptr;
+ softmax_ = softmax;
}
return true;
}
diff --git a/unittest/lstm_layer_test.cc b/unittest/lstm_layer_test.cc
index 2c874995..c0e82307 100644
--- a/unittest/lstm_layer_test.cc
+++ b/unittest/lstm_layer_test.cc
@@ -18,6 +18,8 @@
#include "include_gunit.h"
+#include "input.h"
+#include "lstm.h"
#include "network.h" // for Network, NetworkType
#include "networkio.h"
#include "networkscratch.h"
@@ -173,5 +175,67 @@ TEST(LstmLayerTest, AcceptsConsistentLayer) {
RunForward(net, 1, 2);
}
+// Writes a small LSTM payload using the normal serializers. The child has
+// two inputs and four outputs, independently of its runtime C++ type.
+bool WriteSoftmaxLstmPayload(NetworkType type, const Network &child,
+ TFile *fp) {
+ const int32_t feedback = type == NT_LSTM_SOFTMAX ? 4 : 2;
+ const int32_t na = 1 + 2 + feedback;
+ if (!fp->Serialize(&na)) {
+ return false;
+ }
+ WeightMatrix gate;
+ gate.InitWeightsFloat(2, na + 1, false, 0.0f, nullptr);
+ for (int g = 0; g < 4; ++g) {
+ if (!gate.Serialize(false, fp)) {
+ return false;
+ }
+ }
+ return child.Serialize(fp);
+}
+
+TEST(LstmLayerTest, RejectsNonFullyConnectedSoftmaxChild) {
+ for (NetworkType type : {NT_LSTM_SOFTMAX, NT_LSTM_SOFTMAX_ENCODED}) {
+ SCOPED_TRACE(type);
+ Input child("Input", 2, 4);
+ child.SetEnableTraining(TS_DISABLED);
+ std::vector<char> bytes;
+ TFile writer;
+ writer.OpenWrite(&bytes);
+ ASSERT_TRUE(WriteSoftmaxLstmPayload(type, child, &writer));
+ TFile reader;
+ ASSERT_TRUE(reader.Open(bytes.data(), bytes.size()));
+ LSTM layer("LSTM", 1, 2, 4, false, type);
+ layer.SetEnableTraining(TS_DISABLED);
+ EXPECT_FALSE(layer.DeSerialize(&reader));
+ }
+}
+
+// A valid child must support deserialization and normal forward initialization.
+TEST(LstmLayerTest, AcceptsValidFullyConnectedSoftmaxChild) {
+ for (NetworkType type : {NT_LSTM_SOFTMAX, NT_LSTM_SOFTMAX_ENCODED}) {
+ SCOPED_TRACE(type);
+ FullyConnected child("Softmax", 2, 4, NT_SOFTMAX);
+ child.InitWeights(0.0f, nullptr);
+ child.SetEnableTraining(TS_DISABLED);
+ std::vector<char> bytes;
+ TFile writer;
+ writer.OpenWrite(&bytes);
+ ASSERT_TRUE(WriteSoftmaxLstmPayload(type, child, &writer));
+ TFile reader;
+ ASSERT_TRUE(reader.Open(bytes.data(), bytes.size()));
+ LSTM layer("LSTM", 1, 2, 4, false, type);
+ layer.SetEnableTraining(TS_DISABLED);
+ ASSERT_TRUE(layer.DeSerialize(&reader));
+ NetworkIO input = MakeInput(1, 2);
+ input.Zero();
+ NetworkScratch scratch;
+ NetworkIO output;
+ layer.Forward(false, input, nullptr, &scratch, &output);
+ EXPECT_EQ(output.NumFeatures(), 4);
+ EXPECT_EQ(output.Width(), input.Width());
+ }
+}
+
} // namespace
} // namespace tesseract