Hi OpenFHE team,
I would like to report a reproducible correctness bug in OpenFHE’s public matrix conversion API:
Matrix<int32_t> ConvertToInt32(const Matrix<BigInteger>& input,
const BigInteger& modulus);
The function is documented as converting values from Z_q to the centered range [-q/2, q/2], but it can return a negative int32_t for a residue whose mathematical centered representative is positive but lies outside the int32_t range.
Summary
The public declarations are:
src/core/include/math/matrix.h: lines 785-795
The implementation uses ConvertToInt() and assigns directly into int32_t:
result(i, j) = input(i, j).ConvertToInt();
and similarly for Matrix<BigVector>.
On the tested backend configuration (NATIVE_SIZE=64, MATHBACKEND=4), BasicInteger is uint64_t, so ConvertToInt() returns uint64_t.
If:
INT32_MAX < value <= floor(modulus / 2)
the value is still positive in centered form, but the assignment to int32_t can produce an implementation-defined narrowing result. With Clang 14 on the tested x86-64 configuration, 2147483648 becomes -2147483648.
I reproduced this on:
- tested revision:
ed361af22049007db2107e7c69bcff209e8c420d
The affected source file matches the v1.5.1 release for this code path.
The corresponding MatrixStrassen conversion helpers contain the same unchecked narrowing pattern. I have not separately reproduced those overloads in this report.
Environment
- OpenFHE tested revision:
ed361af22049007db2107e7c69bcff209e8c420d - Matching public release for this code path:
v1.5.1 - Backend configuration:
MATHBACKEND=4,NATIVE_SIZE=64 - Library configuration:
BUILD_STATIC=ON,BUILD_SHARED=OFF - OS: Linux x86_64
- Compiler: Clang
14.0.0
Minimal reproduction
The following program uses the public ConvertToInt32(...) API with:
- modulus
q = 4294967311 - matrix entry
a = 2147483648 = 2^31
Since:
floor(q / 2) = 2147483655
the mathematical centered representative is still positive.
#include "math/matrix.h"
#include "openfhecore.h"
#include <cstdint>
#include <iostream>
using namespace lbcrypto;
int main() {
const BigInteger modulus("4294967311");
const BigInteger coeff("2147483648");
Matrix<BigInteger> input([]() { return BigInteger(0); }, 1, 1);
input(0, 0) = coeff;
Matrix<int32_t> output = ConvertToInt32(input, modulus);
const int64_t mathematicalCenteredValue = 2147483648LL;
const int64_t observed = static_cast<int64_t>(output(0, 0));
std::cout << "modulus=" << modulus << std::endl;
std::cout << "coeff=" << coeff << std::endl;
std::cout << "threshold=" << (modulus / BigInteger(2)) << std::endl;
std::cout << "observed_int32=" << observed << std::endl;
std::cout << "mathematical_centered_value="
<< mathematicalCenteredValue << std::endl;
std::cout << "sign_corrupted=" << (observed < 0 ? 1 : 0) << std::endl;
return observed < 0 ? 1 : 0;
}
A minimal CMakeLists.txt used for this probe is:
cmake_minimum_required(VERSION 3.16)
project(openfhe_matrix_converttoint32_probe CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(OPENFHE_SRC_DIR "/path/to/openfhe-development")
set(OPENFHE_BUILD_DIR "/path/to/openfhe-build")
add_executable(openfhe_matrix_converttoint32_probe
openfhe_matrix_converttoint32_probe.cpp)
target_include_directories(openfhe_matrix_converttoint32_probe PRIVATE
"${OPENFHE_SRC_DIR}/third-party/include"
"${OPENFHE_SRC_DIR}/third-party/cereal/include"
"${OPENFHE_SRC_DIR}/src/core/include"
"${OPENFHE_BUILD_DIR}/src/core"
"${OPENFHE_SRC_DIR}/src/core/lib")
target_link_libraries(openfhe_matrix_converttoint32_probe PRIVATE
"${OPENFHE_BUILD_DIR}/lib/libOPENFHEcore_static.a"
pthread
dl
m)
The OpenFHE library build used by this probe was configured with:
cmake -S /path/to/openfhe-development \
-B /path/to/openfhe-build \
-DCMAKE_CXX_COMPILER=clang++-14 \
-DBUILD_STATIC=ON \
-DBUILD_SHARED=OFF \
-DMATHBACKEND=4 \
-DNATIVE_SIZE=64
Representative build steps:
cmake -S . -B build \
-DCMAKE_CXX_COMPILER=clang++-14
cmake --build build -j
./build/openfhe_matrix_converttoint32_probe
Actual behavior
The probe returns failure and prints:
modulus=4294967311
coeff=2147483648
threshold=2147483655
observed_int32=-2147483648
mathematical_centered_value=2147483648
sign_corrupted=1
So the mathematical centered representative is positive, but the API returns a negative int32_t.
Expected behavior
The mathematical centered representative here is +2147483648, which is not representable as int32_t.
Therefore, the current API cannot return the correct value for this input. It should either:
- reject the conversion before narrowing, with a documented representability
precondition; or - provide an API returning a wider signed type.
Cause analysis
The public declarations are:
src/core/include/math/matrix.h: lines 785-795
The implementation is:
Matrix<int32_t> ConvertToInt32(const Matrix<BigInteger>& input,
const BigInteger& modulus) {
...
BigInteger negativeThreshold(modulus / BigInteger(2));
...
if (input(i, j) > negativeThreshold) {
result(i, j) = -1 * (modulus - input(i, j)).ConvertToInt();
}
else {
result(i, j) = input(i, j).ConvertToInt();
}
}
In the tested backend configuration (NATIVE_SIZE=64, MATHBACKEND=4), BasicInteger is uint64_t, and ConvertToInt() defaults to that backend type:
template <typename T = BasicInteger>
T ConvertToInt() const noexcept
So the positive branch effectively does:
BigInteger(2147483648)
-> uint64_t(2147483648)
-> unchecked conversion to int32_t
-> implementation-defined result
without checking whether the centered value actually fits in signed 32-bit range.
Impact
This is a public result-integrity bug in a documented conversion helper.
It affects callers that pass residues whose mathematical centered representatives fall outside the int32_t range. The public API neither documents such inputs as invalid nor reports that the result is unrepresentable.
The confirmed impact on the tested configuration is:
- silent sign corruption in the returned
Matrix<int32_t>
I have not yet traced a higher-level cryptographic failure from this helper in this report, but this conversion API is part of the public math/lattice layer and can taint any downstream workflow that assumes the centered conversion is correct.
Relevant source locations
- Public declarations:
https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/core/include/math/matrix.h#L785-L795 Matrix<BigInteger>andMatrix<BigVector>implementations:
https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/core/lib/math/matrix.cpp#L154-L189- Default
BasicInteger = uint64_ton the tested configuration:
https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/core/include/math/hal/basicint.h#L46-L58 - Default bigint
ConvertToInt()template:
https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/core/include/math/hal/bigintdyn/ubintdyn.h#L715-L727 MatrixStrassenhelpers with the same unchecked narrowing pattern:
https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/core/include/math/matrixstrassen-impl.h#L520-L551
Suggested direction
If the intended contract is truly “convert centered values to int32_t”, the implementation should perform the representability check while the value, or its negative-side magnitude, is still represented as BigInteger, before calling ConvertToInt().
Only after proving that the mathematical centered representative lies in the range:
INT32_MIN <= centered <= INT32_MAX
should the implementation convert it to a fixed-width integer and assign it to the result matrix.
The negative boundary also requires special handling: a magnitude of 2^31 is valid because it represents INT32_MIN, even though positive 2^31 is not representable as int32_t.
Concretely, that means:
- positive branch: require
value <= INT32_MAX - negative branch: let
magnitude = modulus - value, then requiremagnitude <= 2^31 - if
magnitude == 2^31, map that case specifically toINT32_MIN
If larger centered values are legitimate inputs, the API should return a wider type instead of silently narrowing into int32_t.
The same cleanup should also remove the current pattern of performing signed-valued centered arithmetic via unsigned ConvertToInt() results in the negative branch.
Reported by Jiang Chao, Beijing University of Posts and Telecommunications