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