Commit 60e3ee5f for tesseract

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

    lstm: reject weights which do not fit a float

    With FAST_FLOAT (the default) WeightMatrix::DeSerialize() reads the
    weights of a float matrix and the scales of an int matrix as double
    and narrows them to float. A value beyond FLT_MAX, an infinity or a
    NaN raises FE_OVERFLOW or FE_INVALID, which the tesseract executable
    traps when it was built with g++ (SIGFPE in Init()).

    Let the deserialization fail for such a value, as it does for other
    corrupt matrices.

    Add a unit test.

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

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

diff --git a/Makefile.am b/Makefile.am
index 0ba5f4a1..d4122a55 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1256,6 +1256,7 @@ check_PROGRAMS += validator_test
 endif # ENABLE_TRAINING
 check_PROGRAMS += weightmatrix_deserialize_test
 check_PROGRAMS += weightmatrix_intdim_test
+check_PROGRAMS += weightmatrix_range_test
 check_PROGRAMS += weightmatrix_scales_test

 check_PROGRAMS: libtesseract.la libtesseract_training.la
@@ -1643,6 +1644,10 @@ weightmatrix_intdim_test_SOURCES = unittest/weightmatrix_intdim_test.cc
 weightmatrix_intdim_test_CPPFLAGS = $(unittest_CPPFLAGS)
 weightmatrix_intdim_test_LDADD = $(TESS_LIBS)

+weightmatrix_range_test_SOURCES = unittest/weightmatrix_range_test.cc
+weightmatrix_range_test_CPPFLAGS = $(unittest_CPPFLAGS)
+weightmatrix_range_test_LDADD = $(TESS_LIBS)
+
 weightmatrix_scales_test_SOURCES = unittest/weightmatrix_scales_test.cc
 weightmatrix_scales_test_CPPFLAGS = $(unittest_CPPFLAGS)
 weightmatrix_scales_test_LDADD = $(TESS_LIBS)
diff --git a/src/lstm/weightmatrix.cpp b/src/lstm/weightmatrix.cpp
index fe701d05..1a0babf7 100644
--- a/src/lstm/weightmatrix.cpp
+++ b/src/lstm/weightmatrix.cpp
@@ -18,6 +18,8 @@
 #include "weightmatrix.h"

 #include <cassert> // for assert
+#include <cfloat>  // for FLT_MAX
+#include <cstring> // for std::memcpy
 #include "intsimdmatrix.h"
 #include "simddetect.h" // for DotProduct
 #include "statistc.h"
@@ -38,7 +40,25 @@ const TFloat kAdamEpsilon = 1e-8;

 // Utility functions convert between double and float arrays.
 #ifdef FAST_FLOAT
-static void DoubleToFloat(const GENERIC_2D_ARRAY<double> &src, GENERIC_2D_ARRAY<float> &dst) {
+// Returns true if a double read from a model file can be narrowed to float,
+// i.e. it is finite and its magnitude does not exceed FLT_MAX. Narrowing any
+// other value raises FE_OVERFLOW or FE_INVALID, which the tesseract program
+// traps (SIGFPE), so a corrupt model must be rejected before the conversion.
+// The test works on the bit pattern, because even a floating-point comparison
+// with a signaling NaN raises FE_INVALID. With the sign bit cleared, the bit
+// patterns of doubles order like their magnitudes, and infinities and NaNs
+// sort above every finite value.
+static bool FitsFloat(double value) {
+  uint64_t bits;
+  std::memcpy(&bits, &value, sizeof(bits));
+  const double max_float = FLT_MAX;
+  uint64_t max_bits;
+  std::memcpy(&max_bits, &max_float, sizeof(max_bits));
+  return (bits & ~(UINT64_C(1) << 63)) <= max_bits;
+}
+
+// Returns false if src has an element that does not fit a float.
+static bool DoubleToFloat(const GENERIC_2D_ARRAY<double> &src, GENERIC_2D_ARRAY<float> &dst) {
   const auto dim1 = src.dim1();
   const auto dim2 = src.dim2();
   dst.ResizeNoInit(dim1, dim2);
@@ -46,9 +66,13 @@ static void DoubleToFloat(const GENERIC_2D_ARRAY<double> &src, GENERIC_2D_ARRAY<
     const auto *src_i = src[i];
     auto *dst_i = dst[i];
     for (int j = 0; j < dim2; ++j) {
+      if (!FitsFloat(src_i[j])) {
+        return false;
+      }
       dst_i[j] = static_cast<float>(src_i[j]);
     }
   }
+  return true;
 }
 #endif

@@ -71,8 +95,7 @@ static bool DeSerialize(TFile *fp, GENERIC_2D_ARRAY<TFloat> &tfloat_array) {
   if (!double_array.DeSerialize(fp)) {
     return false;
   }
-  DoubleToFloat(double_array, tfloat_array);
-  return true;
+  return DoubleToFloat(double_array, tfloat_array);
 #else
   return tfloat_array.DeSerialize(fp);
 #endif
@@ -332,7 +355,8 @@ bool WeightMatrix::DeSerialize(bool training, TFile *fp) {
     scales_.reserve(size);
     for (auto n = size; n > 0; n--) {
       double val;
-      if (!fp->DeSerialize(&val)) {
+      // The scales are narrowed to float like the weights of a float matrix.
+      if (!fp->DeSerialize(&val) || !FitsFloat(val)) {
         return false;
       }
       scales_.push_back(val / INT8_MAX);
diff --git a/unittest/weightmatrix_range_test.cc b/unittest/weightmatrix_range_test.cc
new file mode 100644
index 00000000..deb10704
--- /dev/null
+++ b/unittest/weightmatrix_range_test.cc
@@ -0,0 +1,117 @@
+///////////////////////////////////////////////////////////////////////
+// File:        weightmatrix_range_test.cc
+// Description: Tests that a WeightMatrix whose serialized doubles do not
+//              fit a float (beyond FLT_MAX, infinite or NaN) is rejected
+//              by WeightMatrix::DeSerialize when built with FAST_FLOAT
+//              (the default), instead of being narrowed to float with a
+//              floating-point exception that the tesseract program traps.
+//
+// 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 "serialis.h"                 // for TFile
+#include "weightmatrix.h"             // for WeightMatrix
+#include "weightmatrix_test_helper.h" // for DeserializeWeightMatrix
+
+#include <cfloat>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+using namespace unittest;
+
+// Overwrites the double stored at the given byte offset of a blob.
+void PutDouble(std::vector<char> &blob, size_t offset, double value) {
+  ASSERT_LE(offset + sizeof(value), blob.size());
+  std::memcpy(&blob[offset], &value, sizeof(value));
+}
+
+// Serializes a valid float-mode WeightMatrix with WeightMatrix::Serialize
+// and replaces its first weight. Layout as written by Serialize: the mode
+// byte, then the wf_ (GENERIC_2D_ARRAY<double>) Serialize: int32 dim1,
+// int32 dim2, empty_ (double) and dim1*dim2 doubles.
+std::vector<char> MakeFloatWeightMatrix(double first_weight) {
+  WeightMatrix m;
+  m.InitWeightsFloat(3, 4, false, 1.0f, nullptr);
+  std::vector<char> blob;
+  TFile fp;
+  fp.OpenWrite(&blob);
+  EXPECT_TRUE(m.Serialize(false, &fp));
+  PutDouble(blob, 1 + 4 + 4 + 8, first_weight);
+  return blob;
+}
+
+// Builds an int-mode WeightMatrix blob (2 x 2, two scales) and replaces
+// its first scale. Layout as in MakeIntWeightMatrix: the mode byte, int32
+// dim1, int32 dim2, empty_ (int8), dim1*dim2 int8 cells and the scale
+// count (uint32) before the scales.
+std::vector<char> MakeIntWeightMatrixWithScale(double first_scale) {
+  std::vector<char> blob = MakeIntWeightMatrix(2, 2, 2);
+  PutDouble(blob, 1 + 4 + 4 + 1 + 2 * 2 + 4, first_scale);
+  return blob;
+}
+
+// Weights that fit a float must still be accepted.
+TEST(WeightMatrixRangeTest, AcceptsWeightsInFloatRange) {
+  WeightMatrix m;
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(1.0), &m));
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(-1.0), &m));
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(0.0), &m));
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(FLT_MAX), &m));
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(-FLT_MAX), &m));
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeFloatWeightMatrix(1e-300), &m));
+}
+
+// A finite weight beyond FLT_MAX overflows when narrowed to float.
+TEST(WeightMatrixRangeTest, RejectsWeightsBeyondFloatRange) {
+#ifdef FAST_FLOAT
+  WeightMatrix m;
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(1e300), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(-1e300), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(3.5e38), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(
+      MakeFloatWeightMatrix(std::nextafter(static_cast<double>(FLT_MAX), 1e300)), &m));
+#else
+  GTEST_SKIP() << "FAST_FLOAT not enabled; weights are not narrowed to float";
+#endif
+}
+
+// Infinities and NaNs, quiet or signaling, are not usable weights either.
+TEST(WeightMatrixRangeTest, RejectsNonFiniteWeights) {
+#ifdef FAST_FLOAT
+  const uint64_t snan_bits = UINT64_C(0x7ff4000000000000);
+  double snan;
+  std::memcpy(&snan, &snan_bits, sizeof(snan));
+  WeightMatrix m;
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(INFINITY), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(-INFINITY), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(NAN), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeFloatWeightMatrix(snan), &m));
+#else
+  GTEST_SKIP() << "FAST_FLOAT not enabled; weights are not narrowed to float";
+#endif
+}
+
+// The scales of an int-mode matrix are narrowed to float the same way.
+TEST(WeightMatrixRangeTest, RejectsScalesBeyondFloatRange) {
+  WeightMatrix m;
+  EXPECT_TRUE(DeserializeWeightMatrix(MakeIntWeightMatrixWithScale(1.0), &m));
+#ifdef FAST_FLOAT
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeIntWeightMatrixWithScale(1e300), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeIntWeightMatrixWithScale(INFINITY), &m));
+  EXPECT_FALSE(DeserializeWeightMatrix(MakeIntWeightMatrixWithScale(NAN), &m));
+#endif
+}
+
+} // namespace
+} // namespace tesseract