Commit 8bde475d for tesseract

commit 8bde475d4792ba0d4d5bd7b7259b5405613770a5
Author: Stefan Weil <sw@weilnetz.de>
Date:   Thu Oct 8 12:27:42 2026 +0200

    Reject int-mode weight matrices with zero inputs in the deserializer (#4644)

    WeightMatrix::DeSerialize handed the quantized int8 weight matrix to
    IntSimdMatrix::Init without checking its second dimension. That dimension
    is the number of inputs plus the bias column, so with a dim2 of 0 the
    Init code computed num_in = dim2 - 1 = -1 and read the bias weights via
    w(output, num_in) on the empty, null-backed array, crashing the process
    during TessBaseAPI::Init with an out-of-bounds read on the default LSTM
    engine.

    Reject a matrix with dim2 < 1 right after it is read, before the SIMD
    backend consumes it. The FullyConnected/LSTM layer dimension checks that
    exist in the callers run after the matrix has already been consumed and
    cannot catch this.

    Add weightmatrix_intdim_test, which drives WeightMatrix::DeSerialize
    directly with crafted int-mode blobs: a zero second dimension is
    rejected (and without the fix reproduces the SEGV in IntSimdMatrix::Init)
    while a valid matrix still deserializes.

    Fixes GHSA-9vwv-49m8-jv9m

    Reported-by: Ammar Saper Alzain Mohamed <ammarsaper10@gmail.com>
    Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/Makefile.am b/Makefile.am
index 51f26fb6..fb2a5df4 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1238,6 +1238,7 @@ check_PROGRAMS += validate_khmer_test
 check_PROGRAMS += validate_myanmar_test
 check_PROGRAMS += validator_test
 endif # ENABLE_TRAINING
+check_PROGRAMS += weightmatrix_intdim_test

 check_PROGRAMS: libtesseract.la libtesseract_training.la

@@ -1566,6 +1567,10 @@ validator_test_SOURCES = unittest/validator_test.cc
 validator_test_CPPFLAGS = $(unittest_CPPFLAGS)
 validator_test_LDADD = $(TRAINING_LIBS) $(ICU_UC_LIBS)

+weightmatrix_intdim_test_SOURCES = unittest/weightmatrix_intdim_test.cc
+weightmatrix_intdim_test_CPPFLAGS = $(unittest_CPPFLAGS)
+weightmatrix_intdim_test_LDADD = $(TESS_LIBS)
+
 # for windows
 if T_WIN
 apiexample_test_LDADD += -lws2_32
diff --git a/src/lstm/weightmatrix.cpp b/src/lstm/weightmatrix.cpp
index ef21daef..026d51b8 100644
--- a/src/lstm/weightmatrix.cpp
+++ b/src/lstm/weightmatrix.cpp
@@ -291,6 +291,14 @@ bool WeightMatrix::DeSerialize(bool training, TFile *fp) {
     if (!wi_.DeSerialize(fp)) {
       return false;
     }
+    // The second dimension is the number of inputs plus the bias column, so
+    // it must be at least 1. A zero dimension would make IntSimdMatrix::Init
+    // compute num_in = dim2 - 1 = -1 and read the bias from array_[-1] on an
+    // unallocated (null) array, so reject the corrupt model instead of
+    // crashing.
+    if (wi_.dim2() < 1) {
+      return false;
+    }
     uint32_t size;
     if (!fp->DeSerialize(&size)) {
       return false;
diff --git a/unittest/weightmatrix_intdim_test.cc b/unittest/weightmatrix_intdim_test.cc
new file mode 100644
index 00000000..41071a58
--- /dev/null
+++ b/unittest/weightmatrix_intdim_test.cc
@@ -0,0 +1,101 @@
+///////////////////////////////////////////////////////////////////////
+// File:        weightmatrix_intdim_test.cc
+// Description: Tests that an int-mode WeightMatrix whose second
+//              dimension is 0 is rejected by WeightMatrix::DeSerialize
+//              instead of crashing in IntSimdMatrix::Init (which would
+//              compute num_in = dim2 - 1 = -1 and read the bias from
+//              array_[-1] on an unallocated array).
+//
+// 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 "intsimdmatrix.h" // for IntSimdMatrix (active backend)
+#include "serialis.h"      // for TFile
+#include "weightmatrix.h"  // for WeightMatrix
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Appends little-endian values to a byte buffer.
+class WmWriter {
+ public:
+  void PutU8(uint8_t v) { data_.push_back(static_cast<char>(v)); }
+  void PutI32(int32_t v) {
+    for (int i = 0; i < 4; ++i) data_.push_back(static_cast<char>((v >> (8 * i)) & 0xFF));
+  }
+  void PutU32(uint32_t v) { PutI32(static_cast<int32_t>(v)); }
+  const std::vector<char> &data() const { return data_; }
+
+ private:
+  std::vector<char> data_;
+};
+
+// Builds an int-mode (double/float format, kDoubleFlag | kInt8Flag)
+// WeightMatrix blob with the given wi_ dimensions and scale vector.
+// Layout mirrors WeightMatrix::DeSerialize: mode byte, then the wi_
+// (GENERIC_2D_ARRAY<int8_t>) DeSerialize: int32 dim1, int32 dim2, empty_
+// (int8) and dim1*dim2 int8 cells, then the scale count (uint32) and the
+// scales as doubles.
+std::vector<char> MakeIntWeightMatrix(int32_t dim1, int32_t dim2, int32_t num_scales) {
+  WmWriter w;
+  w.PutU8(128 + 1); // kDoubleFlag | kInt8Flag
+  w.PutI32(dim1);
+  w.PutI32(dim2);
+  w.PutU8(0); // empty_ (unused cell)
+  for (int32_t i = 0; i < dim1 * dim2; ++i) {
+    w.PutU8(0);
+  }
+  w.PutU32(static_cast<uint32_t>(num_scales));
+  for (int32_t i = 0; i < num_scales; ++i) {
+    union {
+      double d;
+      uint64_t u;
+    } conv;
+    conv.d = 1.0;
+    w.PutU32(static_cast<uint32_t>(conv.u & 0xFFFFFFFF));
+    w.PutU32(static_cast<uint32_t>(conv.u >> 32));
+  }
+  return w.data();
+}
+
+class WeightMatrixIntDimTest : public testing::Test {
+ protected:
+  // Runs DeSerialize on the given blob. Only meaningful when a SIMD int
+  // backend is active, since that is what triggers the vulnerable Init path.
+  bool DeSerialize(const std::vector<char> &blob, WeightMatrix *m) {
+    TFile fp;
+    fp.Open(blob.data(), blob.size());
+    if (fp.RemainingBytes() != blob.size()) {
+      return false;
+    }
+    return m->DeSerialize(false, &fp);
+  }
+};
+
+// A valid int-mode matrix must still deserialize successfully.
+TEST_F(WeightMatrixIntDimTest, AcceptsValidIntMatrix) {
+  WeightMatrix m;
+  EXPECT_TRUE(DeSerialize(MakeIntWeightMatrix(2, 2, 2), &m));
+}
+
+// A zero second dimension must be rejected instead of crashing in Init.
+TEST_F(WeightMatrixIntDimTest, RejectsZeroDim2) {
+  if (IntSimdMatrix::intSimdMatrix == nullptr) {
+    GTEST_SKIP() << "no SIMD int backend active; the vulnerable path is unreachable";
+  }
+  WeightMatrix m;
+  EXPECT_FALSE(DeSerialize(MakeIntWeightMatrix(2, 0, 0), &m));
+}
+
+} // namespace
+} // namespace tesseract