Commit 1f7adfad for tesseract

commit 1f7adfad28ee06afdd9d4e1b1a807c87c07c796b
Author: Nathan Keys <professor.moody@pm.me>
Date:   Wed Oct 28 18:15:25 2026 -0500

    Validate Plumbing child dimensions during deserialization

    Plumbing::DeSerialize currently passes each successfully loaded child
    directly to AddToStack. A malformed series or parallel stack can
    therefore reach dimension assertions, and a parallel output sum can
    overflow the signed int used for the combined output count.

    Validate every child after the first before transferring ownership.
    Series children must accept the preceding output count. Parallel
    children must share the parent input count, and their combined output
    count must remain representable. Delete a rejected child and return
    false so Network::CreateFromFile propagates the load failure normally.

    Add six focused tests in unittest/plumbing_dimensions_test.cc for
    series and parallel mismatches, output-sum overflow, the exact
    signed-int boundary, a valid series, and a valid parallel round trip.

    Signed-off-by: Nathan Keys <professor.moody@pm.me>

diff --git a/src/lstm/plumbing.cpp b/src/lstm/plumbing.cpp
index 908a7513..702390be 100644
--- a/src/lstm/plumbing.cpp
+++ b/src/lstm/plumbing.cpp
@@ -18,6 +18,8 @@

 #include "plumbing.h"

+#include <limits>
+
 #include <utility> // for std::move

 namespace tesseract {
@@ -239,6 +241,15 @@ bool Plumbing::DeSerialize(TFile *fp) {
     if (network == nullptr) {
       return false;
     }
+    // Validate the dimensions before AddToStack combines the child layers.
+    if (!stack_.empty() &&
+        ((type_ == NT_SERIES && no_ != network->NumInputs()) ||
+         (type_ != NT_SERIES &&
+          (ni_ != network->NumInputs() ||
+           network->NumOutputs() > std::numeric_limits<int>::max() - no_)))) {
+      delete network;
+      return false;
+    }
     AddToStack(network);
   }
   if ((network_flags_ & NF_LAYER_SPECIFIC_LR) && !fp->DeSerialize(learning_rates_)) {
diff --git a/unittest/plumbing_dimensions_test.cc b/unittest/plumbing_dimensions_test.cc
new file mode 100644
index 00000000..01893ea3
--- /dev/null
+++ b/unittest/plumbing_dimensions_test.cc
@@ -0,0 +1,98 @@
+///////////////////////////////////////////////////////////////////////
+// File:        plumbing_dimensions_test.cc
+// Description: Serialized child-layer dimension consistency tests.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+///////////////////////////////////////////////////////////////////////
+
+#include "include_gunit.h"
+
+#include "parallel.h"
+#include "reconfig.h"
+#include "series.h"
+
+#include <limits>
+#include <memory>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Each child is valid independently. The parent must check their dimensions.
+bool ReadChildren(Plumbing *parent, const Network &first,
+                  const Network &second) {
+  std::vector<char> data;
+  TFile writer;
+  writer.OpenWrite(&data);
+  const uint32_t count = 2;
+  EXPECT_TRUE(writer.Serialize(&count));
+  EXPECT_TRUE(first.Serialize(&writer));
+  EXPECT_TRUE(second.Serialize(&writer));
+  TFile reader;
+  EXPECT_TRUE(reader.Open(data.data(), data.size()));
+  return parent->DeSerialize(&reader);
+}
+
+TEST(PlumbingDimensionsTest, RejectsSeriesDimensionMismatch) {
+  Reconfig first("first", 2, 3, 1);
+  Reconfig second("second", 3, 1, 1);
+  Series parent("series");
+  EXPECT_FALSE(ReadChildren(&parent, first, second));
+}
+
+TEST(PlumbingDimensionsTest, RejectsParallelDimensionMismatch) {
+  Reconfig first("first", 2, 1, 1);
+  Reconfig second("second", 3, 1, 1);
+  Parallel parent("parallel", NT_PARALLEL);
+  EXPECT_FALSE(ReadChildren(&parent, first, second));
+}
+
+TEST(PlumbingDimensionsTest, RejectsParallelOutputSumBeyondIntRange) {
+  Reconfig first("first", 1, std::numeric_limits<int>::max(), 1);
+  Reconfig second("second", 1, 1, 1);
+  Parallel parent("parallel", NT_PARALLEL);
+  EXPECT_FALSE(ReadChildren(&parent, first, second));
+}
+
+TEST(PlumbingDimensionsTest, AcceptsParallelOutputSumAtIntLimit) {
+  Reconfig first("first", 1, std::numeric_limits<int>::max() - 1, 1);
+  Reconfig second("second", 1, 1, 1);
+  Parallel parent("parallel", NT_PARALLEL);
+  ASSERT_TRUE(ReadChildren(&parent, first, second));
+  EXPECT_EQ(1, parent.NumInputs());
+  EXPECT_EQ(std::numeric_limits<int>::max(), parent.NumOutputs());
+  EXPECT_EQ(2U, parent.stack().size());
+}
+
+TEST(PlumbingDimensionsTest, AcceptsMatchingSeriesDimensions) {
+  Reconfig first("first", 2, 3, 1);
+  Reconfig second("second", 6, 2, 1);
+  Series parent("series");
+  ASSERT_TRUE(ReadChildren(&parent, first, second));
+  EXPECT_EQ(2, parent.NumInputs());
+  EXPECT_EQ(12, parent.NumOutputs());
+  EXPECT_EQ(2U, parent.stack().size());
+}
+
+TEST(PlumbingDimensionsTest, RoundTripsValidParallelLayers) {
+  Parallel original("parallel", NT_PARALLEL);
+  original.AddToStack(new Reconfig("first", 2, 3, 1));
+  original.AddToStack(new Reconfig("second", 2, 2, 1));
+  std::vector<char> data;
+  TFile writer;
+  writer.OpenWrite(&data);
+  ASSERT_TRUE(original.Serialize(&writer));
+  TFile reader;
+  ASSERT_TRUE(reader.Open(data.data(), data.size()));
+  std::unique_ptr<Network> restored(Network::CreateFromFile(&reader));
+  ASSERT_NE(nullptr, restored);
+  EXPECT_EQ(original.NumInputs(), restored->NumInputs());
+  EXPECT_EQ(original.NumOutputs(), restored->NumOutputs());
+  EXPECT_EQ(0U, reader.RemainingBytes());
+}
+
+} // namespace
+} // namespace tesseract