Commit 812aaf36 for tesseract

commit 812aaf36ca7b948cf670e68e42e782a124aaa715
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Nov 13 17:30:49 2026 +0100

    matrix: keep the padding when a GENERIC_2D_ARRAY is copied

    NetworkIO allocates its int8_t array with padding, because the SIMD
    code for the matrix multiplication reads whole registers beyond the
    last timestep. The copy assignment of GENERIC_2D_ARRAY resized the
    destination without padding, so a copy (like *output = input in
    Input::Forward()) could be read out of bounds by the following layer.

    Remember the padding which was given to ResizeNoInit() and use it for
    copies and moves.

    Add a unit test.

    Fix proposed by the Anthropic OSS Scanner (finding ANT-2026-MZ7IPJ7U).

    Assisted-by: OpenCode / Claude Fable 5.2 (Anthropic)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/ccstruct/matrix.h b/src/ccstruct/matrix.h
index 147634db..773027f7 100644
--- a/src/ccstruct/matrix.h
+++ b/src/ccstruct/matrix.h
@@ -77,19 +77,22 @@ public:
   }
   GENERIC_2D_ARRAY(GENERIC_2D_ARRAY<T> &&src) noexcept
       : array_(src.array_), empty_(src.empty_), dim1_(src.dim1_), dim2_(src.dim2_),
-        size_allocated_(src.size_allocated_) {
+        size_allocated_(src.size_allocated_), pad_(src.pad_) {
     src.array_ = nullptr;
     src.empty_ = static_cast<T>(0);
     src.dim1_ = 0;
     src.dim2_ = 0;
     src.size_allocated_ = 0;
+    src.pad_ = 0;
   }
   virtual ~GENERIC_2D_ARRAY() {
     delete[] array_;
   }

   void operator=(const GENERIC_2D_ARRAY<T> &src) {
-    ResizeNoInit(src.dim1(), src.dim2());
+    // Keep the padding of src, as users of the copy may rely on it in the
+    // same way as users of src (eg the SIMD code reading an int8_t array).
+    ResizeNoInit(src.dim1(), src.dim2(), src.pad_);
     int size = num_elements();
     if (size > 0) {
       memcpy(array_, src.array_, size * sizeof(array_[0]));
@@ -103,11 +106,13 @@ public:
       dim1_ = src.dim1_;
       dim2_ = src.dim2_;
       size_allocated_ = src.size_allocated_;
+      pad_ = src.pad_;
       src.array_ = nullptr;
       src.empty_ = static_cast<T>(0);
       src.dim1_ = 0;
       src.dim2_ = 0;
       src.size_allocated_ = 0;
+      src.pad_ = 0;
     }
   }

@@ -144,6 +149,7 @@ public:
     }
     dim1_ = size1;
     dim2_ = size2;
+    pad_ = pad;
     // Fill the padding data so it isn't uninitialized.
     for (int i = new_size - pad; i < new_size; ++i) {
       array_[i] = empty_;
@@ -178,6 +184,7 @@ public:
       dim1_ = size1;
       dim2_ = size2;
       size_allocated_ = new_size;
+      pad_ = 0;
     }
   }

@@ -661,6 +668,9 @@ protected:
   // needed. If Resize is used, memory is retained so it can be re-expanded
   // without a further alloc, and this stores the allocated size.
   int size_allocated_;
+  // The number of extra elements that ResizeNoInit provided beyond the bounds
+  // of the array (dim1_ * dim2_). A copy of the array gets the same padding.
+  int pad_ = 0;
 };

 // A generic class to store a banded triangular matrix with entries of type T.
diff --git a/unittest/matrix_test.cc b/unittest/matrix_test.cc
index d37bfab7..293ed0a7 100644
--- a/unittest/matrix_test.cc
+++ b/unittest/matrix_test.cc
@@ -136,4 +136,31 @@ TEST_F(MatrixTest, RotatingTranspose_0_2) {
   EXPECT_EQ(6, m(15, 0));
 }

+// Tests that a copy of an array keeps the padding requested by ResizeNoInit,
+// as NetworkIO relies on it for the SIMD code reading an int8_t array.
+TEST_F(MatrixTest, CopyKeepsPadding) {
+  class PaddedArray : public GENERIC_2D_ARRAY<int8_t> {
+  public:
+    int size_allocated() const {
+      return size_allocated_;
+    }
+  };
+  const int kDim1 = 5, kDim2 = 3, kPad = 31;
+  PaddedArray src;
+  src.ResizeNoInit(kDim1, kDim2, kPad);
+  src.Clear();
+  PaddedArray assigned;
+  assigned = src;
+  EXPECT_EQ(kDim1, assigned.dim1());
+  EXPECT_EQ(kDim2, assigned.dim2());
+  EXPECT_EQ(kDim1 * kDim2 + kPad, assigned.size_allocated());
+  PaddedArray constructed(src);
+  EXPECT_EQ(kDim1 * kDim2 + kPad, constructed.size_allocated());
+  // The padding also survives a move followed by a copy.
+  PaddedArray moved(std::move(src));
+  assigned = PaddedArray();
+  assigned = moved;
+  EXPECT_EQ(kDim1 * kDim2 + kPad, assigned.size_allocated());
+}
+
 } // namespace tesseract