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