Commit 553a9392 for tesseract
commit 553a9392a6585aea4a6672a4120a6bb69bad8671
Author: Stefan Weil <sw@weilnetz.de>
Date: Fri Nov 13 17:30:49 2026 +0100
lstm: clip before converting to an integer in Tanh and Logistic
Tanh() and Logistic() scaled their argument and converted it to
unsigned before the index was compared with the size of the table.
The conversion of a value which does not fit is undefined: code built
with g++ raises FE_INVALID, which the tesseract executable traps
(SIGFPE), and a truncated index could pass the test, so the result was
far outside of the range of the function. The argument is only bounded
by the weights of the model.
Compare the argument with the saturation point before it is scaled.
NetworkIO::WriteTimeStepPart() had the same pattern (round to int,
then clip), so clip first there, too.
Add a unit test for both functions.
Fix proposed by the Anthropic OSS Scanner (finding ANT-2026-7A38K5B7).
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 4dd8d6fe..0ba5f4a1 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1167,6 +1167,7 @@ check_PROGRAMS += equationdetect_test
endif # !DISABLED_LEGACY_ENGINE
check_PROGRAMS += fileio_test
check_PROGRAMS += fullyconnected_test
+check_PROGRAMS += functions_test
check_PROGRAMS += genericvector_test
check_PROGRAMS += heap_test
check_PROGRAMS += imagedata_test
@@ -1338,6 +1339,10 @@ fullyconnected_test_SOURCES = unittest/fullyconnected_test.cc
fullyconnected_test_CPPFLAGS = $(unittest_CPPFLAGS)
fullyconnected_test_LDADD = $(TESS_LIBS)
+functions_test_SOURCES = unittest/functions_test.cc
+functions_test_CPPFLAGS = $(unittest_CPPFLAGS)
+functions_test_LDADD = $(TESS_LIBS)
+
genericvector_test_SOURCES = unittest/genericvector_test.cc
genericvector_test_CPPFLAGS = $(unittest_CPPFLAGS)
genericvector_test_LDADD = $(TESS_LIBS)
diff --git a/src/lstm/functions.h b/src/lstm/functions.h
index 1e71b2f2..f3cd05de 100644
--- a/src/lstm/functions.h
+++ b/src/lstm/functions.h
@@ -45,11 +45,14 @@ inline TFloat Tanh(TFloat x) {
if (x < 0) {
return -Tanh(-x);
}
- x *= kScaleFactor;
- auto index = static_cast<unsigned>(x);
- if (index >= (kTableSize - 1)) {
+ // Clip before the conversion to unsigned: converting a value which does not
+ // fit is undefined (g++ raises FE_INVALID, which the CLI traps), and a large
+ // x can come from the weights and scales of an untrusted model.
+ if (x >= (kTableSize - 1) / kScaleFactor) {
return 1;
}
+ x *= kScaleFactor;
+ auto index = static_cast<unsigned>(x);
TFloat tanh_i0 = TanhTable[index];
TFloat tanh_i1 = TanhTable[index + 1];
// Linear interpolation.
@@ -60,11 +63,12 @@ inline TFloat Logistic(TFloat x) {
if (x < 0) {
return 1 - Logistic(-x);
}
- x *= kScaleFactor;
- auto index = static_cast<unsigned>(x);
- if (index >= (kTableSize - 1)) {
+ // Clip before the conversion to unsigned, see Tanh() above.
+ if (x >= (kTableSize - 1) / kScaleFactor) {
return 1;
}
+ x *= kScaleFactor;
+ auto index = static_cast<unsigned>(x);
TFloat l0 = LogisticTable[index];
TFloat l1 = LogisticTable[index + 1];
// Linear interpolation.
diff --git a/src/lstm/networkio.cpp b/src/lstm/networkio.cpp
index 7a31034b..e72bb731 100644
--- a/src/lstm/networkio.cpp
+++ b/src/lstm/networkio.cpp
@@ -667,7 +667,9 @@ void NetworkIO::WriteTimeStepPart(int t, int offset, int num_features, const TFl
if (int_mode_) {
int8_t *line = i_[t] + offset;
for (int i = 0; i < num_features; ++i) {
- line[i] = ClipToRange<int>(IntCastRounded(input[i] * INT8_MAX), -INT8_MAX, INT8_MAX);
+ // Clip before the conversion to int: the output of a linear layer of an
+ // untrusted model is not bounded to [-1, 1].
+ line[i] = IntCastRounded(ClipToRange<TFloat>(input[i] * INT8_MAX, -INT8_MAX, INT8_MAX));
}
} else {
float *line = f_[t] + offset;
diff --git a/unittest/functions_test.cc b/unittest/functions_test.cc
new file mode 100644
index 00000000..2a4db168
--- /dev/null
+++ b/unittest/functions_test.cc
@@ -0,0 +1,64 @@
+///////////////////////////////////////////////////////////////////////
+// File: functions_test.cc
+// Description: Tests that the table based Tanh() and Logistic() saturate
+// for huge arguments without an undefined conversion to an
+// integer, and that they still interpolate in range.
+//
+// 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 "functions.h"
+
+#include <cfenv>
+#include <cmath>
+
+namespace tesseract {
+
+// Regression test for the table based Tanh() and Logistic().
+//
+// Their argument is the output of WeightMatrix::MatrixDotVector() and is only
+// bounded by the weights and scales of the model, which are loaded without a
+// range check. The functions used to scale the argument by kScaleFactor and
+// convert it to unsigned *before* comparing the index with the table size;
+// converting a value which does not fit is undefined, raises FE_INVALID
+// (which the tesseract CLI traps when built with g++) and, below 2^63, yields
+// a truncated index that can pass the check so that the result was far
+// outside the range of the function.
+
+// Arguments that saturate, including ones for which 256 * x does not fit
+// into a 32 bit or 64 bit integer.
+static const TFloat kLargeArgs[] = {16.0f, 1e3f, 1e6f, 1e10f, 1e16f, 1e20f, 3e38f};
+
+TEST(FunctionsTest, TanhSaturates) {
+ std::feclearexcept(FE_ALL_EXCEPT);
+ for (TFloat x : kLargeArgs) {
+ EXPECT_FLOAT_EQ(1, Tanh(x)) << "x=" << x;
+ EXPECT_FLOAT_EQ(-1, Tanh(-x)) << "x=" << -x;
+ }
+ EXPECT_EQ(0, std::fetestexcept(FE_INVALID));
+}
+
+TEST(FunctionsTest, LogisticSaturates) {
+ std::feclearexcept(FE_ALL_EXCEPT);
+ for (TFloat x : kLargeArgs) {
+ EXPECT_FLOAT_EQ(1, Logistic(x)) << "x=" << x;
+ EXPECT_FLOAT_EQ(0, Logistic(-x)) << "x=" << -x;
+ }
+ EXPECT_EQ(0, std::fetestexcept(FE_INVALID));
+}
+
+// Arguments inside the table still interpolate.
+TEST(FunctionsTest, InterpolatesInRange) {
+ for (TFloat x = -15.0f; x <= 15.0f; x += 0.37f) {
+ EXPECT_NEAR(std::tanh(x), Tanh(x), 1e-3) << "x=" << x;
+ EXPECT_NEAR(1 / (1 + std::exp(-x)), Logistic(x), 1e-3) << "x=" << x;
+ }
+}
+
+} // namespace tesseract