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