Commit b31e8585 for tesseract

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

    classify: avoid a float overflow in ReadParamDesc

    Range and MidRange were calculated in float from the limits Min and
    Max of a normproto file. For limits like -3e38 and 3e38 the difference
    overflows, and the tesseract executable traps FE_OVERFLOW when it was
    built with g++ (SIGFPE in Init()).

    Calculate in double and clip the range to the range of float.

    Add a unit test. ReadParamDesc() gets TESS_API for it, as the test
    must also link with a shared library which hides its symbols.

    Based on the fix proposed by the Anthropic OSS Scanner
    (finding ANT-2026-0INCQPMJ).

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

diff --git a/src/classify/clusttool.cpp b/src/classify/clusttool.cpp
index e11c9374..d14d9971 100644
--- a/src/classify/clusttool.cpp
+++ b/src/classify/clusttool.cpp
@@ -19,6 +19,8 @@

 #include "clusttool.h"

+#include <algorithm> // for std::clamp
+#include <cfloat>  // for FLT_MAX
 #include <cmath>   // for M_PI, std::isnan
 #include <locale>  // for std::locale::classic
 #include <sstream> // for std::stringstream
@@ -149,9 +151,14 @@ PARAM_DESC *ReadParamDesc(TFile *fp, uint16_t N) {
     ASSERT_HOST(!stream.fail());
     ParamDesc[i].Circular = (linear_token[0] == 'c');
     ParamDesc[i].NonEssential = (essential_token[0] != 'e');
-    ParamDesc[i].Range = ParamDesc[i].Max - ParamDesc[i].Min;
+    // Min and Max come from the file and can be any float value, so compute
+    // in double and clip the range to avoid a floating point overflow
+    // (which the tesseract program traps with SIGFPE).
+    const double min = ParamDesc[i].Min;
+    const double max = ParamDesc[i].Max;
+    ParamDesc[i].Range = std::clamp<double>(max - min, -FLT_MAX, FLT_MAX);
     ParamDesc[i].HalfRange = ParamDesc[i].Range / 2;
-    ParamDesc[i].MidRange = (ParamDesc[i].Max + ParamDesc[i].Min) / 2;
+    ParamDesc[i].MidRange = (max + min) / 2;
   }
   return (ParamDesc);
 }
diff --git a/src/classify/clusttool.h b/src/classify/clusttool.h
index 6cca04af..c6b5edcc 100644
--- a/src/classify/clusttool.h
+++ b/src/classify/clusttool.h
@@ -28,6 +28,7 @@ namespace tesseract {

 uint16_t ReadSampleSize(tesseract::TFile *fp);

+TESS_API
 PARAM_DESC *ReadParamDesc(tesseract::TFile *fp, uint16_t N);

 PROTOTYPE *ReadPrototype(tesseract::TFile *fp, uint16_t N);
diff --git a/unittest/normproto_test.cc b/unittest/normproto_test.cc
index b0f1b5c4..be94325f 100644
--- a/unittest/normproto_test.cc
+++ b/unittest/normproto_test.cc
@@ -18,9 +18,12 @@
 #include "include_gunit.h"

 #include "classify.h"
+#include "clusttool.h" // for ReadParamDesc
 #include "serialis.h" // for TFile
 #include "util/tempdir.h" // for CreateTempDir, RemoveTempDir

+#include <cfenv> // for std::feclearexcept, std::fetestexcept
+#include <cmath> // for std::isfinite
 #include <cstdio>
 #include <string>
 #include <vector>
@@ -103,5 +106,22 @@ TEST_F(NormprotoTest, ReadsValidNormprotos) {
   EXPECT_EQ(classifier_.NormProtos, nullptr);
 }

+// Parameter limits whose difference or sum does not fit in a float must not
+// raise a floating point overflow (the tesseract program traps it).
+TEST(ParamDescTest, ToleratesHugeLimits) {
+  for (const char *limits : {"-3e38 3e38", "3e38 3e38", "3e38 -3e38", "-3e38 -3e38"}) {
+    std::string line = std::string("linear essential ") + limits + "\n";
+    TFile fp;
+    ASSERT_TRUE(fp.Open(line.data(), line.size()));
+    std::feclearexcept(FE_OVERFLOW);
+    PARAM_DESC *desc = ReadParamDesc(&fp, 1);
+    EXPECT_FALSE(std::fetestexcept(FE_OVERFLOW)) << limits;
+    EXPECT_TRUE(std::isfinite(desc->Range)) << limits;
+    EXPECT_TRUE(std::isfinite(desc->HalfRange)) << limits;
+    EXPECT_TRUE(std::isfinite(desc->MidRange)) << limits;
+    delete[] desc;
+  }
+}
+
 } // namespace
 } // namespace tesseract