I would like to report a reproducible CKKS configuration bug in HElib's public context/key-generation path for high precision(...) settings.
On the checked-out HElib revision, a normal CKKS setup using:
helib::ContextBuilder<helib::CKKS>().precision(54)
or larger can reach an unrepresentable scaling-factor computation in EncryptedArrayCx::encodeScalingFactor() during SecKey::GenSecKey(), causing an NTL abort at 54 and undefined behavior at 55 on the tested build.
Summary
In the checked-out implementation, SecKey::ImportSecKey() computes the public encryption key's CKKS scaling factor with:
pubEncrKey.ratFactor =
pubEncrKey.noiseBound * getContext().getEA().getCx().encodeScalingFactor();
and encodeScalingFactor() defaults to:
precision = (1L << alMod.getR());
...
long f = std::ceil(precision * roundErr);
return (1L << NTL::NextPowerOfTwo(f));
For high CKKS precision values, this can reach an unrepresentable scaling-factor computation before any explicit rejection of the context-derived default.
I reproduced the following behavior:
precision = 53: key generation succeeds normally
precision = 54: key generation aborts with NextPowerOfTwo: overflow
precision = 55: UBSan reports an out-of-range conversion to long in encodeScalingFactor()
- in a separate UBSan recovery-mode run,
precision = 56 reports the same out-of-range conversion class with a larger operand
This occurs in a normal public setup path:
ContextBuilder<CKKS> -> SecKey(context) -> GenSecKey()
without malformed serialized input or manual object corruption.
Environment
- HElib checked-out revision:
3e337a66a91a92d49de6a9505340826b0eb71081
- OS: Linux x86_64
- Compiler:
Clang 14.0.0
- Sanitizers: AddressSanitizer and UndefinedBehaviorSanitizer
- Linked libraries: system
NTL and GMP
NTL_SP_NBITS = 60
NTL_SP_BOUND = 1152921504606846976
sizeof(long) = 8
LONG_MAX = 9223372036854775807
- I confirmed the checked-out copies of
include/helib/EncryptedArray.h, include/helib/Context.h, and src/keys.cpp were clean when running this reproducer.
- The auxiliary include directory
/tmp/helib-system-headers-v1 contains only NTL/ and gmp.h headers used to prefer the system NTL 11 headers; it does not contain HElib headers.
Representative build command:
clang++-14 -std=c++17 -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined \
-I /tmp/helib-system-headers-v1 \
-I HElib/dependencies/json \
-I HElib/include \
-I build-helib-sys-asan3/src \
fuzz/helib_ckks_precision_keygen_probe.cpp \
-L build-helib-sys-asan3/lib \
-Wl,-rpath,/home/sht/agent-fuzzing/build-helib-sys-asan3/lib \
build-helib-sys-asan3/lib/libhelib.a \
-lntl -lgmp -lpthread -ldl -lm \
-o fuzz/bin/helib_ckks_precision_keygen_probe
Minimal reproduction
#include <helib/helib.h>
#include <iostream>
int main(int argc, char** argv)
{
const long precision = (argc >= 2) ? std::stol(argv[1]) : 55;
std::cout << "precision=" << precision << std::endl;
helib::Context context = helib::ContextBuilder<helib::CKKS>()
.m(16384)
.bits(300)
.precision(precision)
.c(2)
.build();
std::cout << "about to generate secret key" << std::endl;
helib::SecKey secret_key(context);
secret_key.GenSecKey();
std::cout << "keygen_ok" << std::endl;
return 0;
}
Control case:
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 \
./fuzz/bin/helib_ckks_precision_keygen_probe 53
Output:
precision=53
NTL_SP_NBITS=60
NTL_SP_BOUND=1152921504606846976
sizeof_long=8
LONG_MAX=9223372036854775807
about to generate secret key
keygen_ok
Failing boundary:
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 \
stdbuf -o0 -e0 ./fuzz/bin/helib_ckks_precision_keygen_probe 54
Output:
precision=54
NTL_SP_NBITS=60
NTL_SP_BOUND=1152921504606846976
sizeof_long=8
LONG_MAX=9223372036854775807
about to generate secret key
NextPowerOfTwo: overflow
UBSan boundary:
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
./fuzz/bin/helib_ckks_precision_keygen_probe 55
Output:
precision=55
NTL_SP_NBITS=60
NTL_SP_BOUND=1152921504606846976
sizeof_long=8
LONG_MAX=9223372036854775807
about to generate secret key
/home/sht/agent-fuzzing/HElib/include/helib/EncryptedArray.h:1310:14: runtime error: 9.41356e+18 is outside the range of representable values of type 'long'
#0 helib::EncryptedArrayDerived<helib::PA_cx>::encodeScalingFactor(...)
#1 helib::SecKey::ImportSecKey(...)
#2 helib::SecKey::GenSecKey(...)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/sht/agent-fuzzing/HElib/include/helib/EncryptedArray.h:1310:14 in
In a separate UBSan recovery-mode run, precision = 56 produced the same class of out-of-range conversion with a larger operand (1.88271e+19) and then continued to print keygen_ok. I do not rely on that continued execution to establish the bug.
Actual behavior
High precision(...) values accepted by the CKKS ContextBuilder can make encodeScalingFactor() abort or trigger undefined behavior during GenSecKey().
At precision = 54, the process aborts in NTL::NextPowerOfTwo.
At precision = 55, UBSan reports an out-of-range conversion to long in encodeScalingFactor().
With UBSan recovery enabled, later execution can continue and print keygen_ok, but that post-UB continuation is not required to establish the bug.
Expected behavior
If the current CKKS implementation does not support these high precision settings, the library should reject them explicitly before key generation reaches overflow or undefined behavior.
At minimum, a normal public ContextBuilder<CKKS> plus GenSecKey() setup should not evaluate overflowing arithmetic or narrowing conversions in its internal scaling-factor computation.
Impact
This affects a normal public configuration path for CKKS.
An honest caller can select a high precision through the documented builder API and trigger either:
- an abort during key generation; or
- undefined behavior during key generation due to an out-of-range floating-to-integer conversion
I have not yet established a downstream wrong-result case from any key material produced after UBSan-recovery execution, but the key-generation path itself already reaches overflow and undefined behavior on a normal public configuration path.
Cause analysis
The checked-out implementation of encodeScalingFactor() is:
long encodeScalingFactor(long precision = -1, double roundErr = -1.0) const
{
assertTrue<InvalidArgument>(precision < NTL_SP_BOUND,
"Precision exceeds max single precision bound");
if (precision <= 0)
precision = (1L << alMod.getR());
if (roundErr < 0)
roundErr = encodeRoundingError();
long f = std::ceil(precision * roundErr);
return (1L << NTL::NextPowerOfTwo(f));
}
Two different quantities are called "precision" here:
- the public
ContextBuilder<CKKS>::precision(r) parameter is a bit precision
encodeScalingFactor() then converts that bit precision into the integer quantity 2^r
So when SecKey::ImportSecKey() uses the default encodeScalingFactor(), the context bit precision r is converted into:
where alMod.getR() is the CKKS bit precision configured by the public builder.
The existing precision < NTL_SP_BOUND check does not validate that context-derived default value: it runs while precision is still the -1 sentinel, before precision is replaced with 1L << alMod.getR().
That validation-order gap is real, but it is not by itself a sufficient explanation for the observed 54/55 boundary on this build. In the tested NTL configuration above, both 2^54 and 2^55 are still below NTL_SP_BOUND.
The more direct failure condition is that the helper never validates the full representability of:
or of the final rounded power-of-two scaling factor before performing the narrowing conversion to long or forming a final power-of-two scaling factor that is not representable as a positive long.
For the reproducer parameters above:
m = 16384, so phi(m) = 8192
- the builder default is
scale_ = 10
encodeRoundingError() returns noiseBoundForUniform(0.5, phim)
noiseBoundForUniform() computes scale * sqrt(degBound / 3.0) * magBound
So the default rounding-error bound here is:
roundErr = 10 * sqrt(8192 / 3) * 0.5
≈ 261.2789059
That makes the key boundaries:
r = 53: ceil(2^r * roundErr) ≈ 2.3534e18
Next power of two is 2^62, which still fits in signed long
r = 54: ceil(2^r * roundErr) ≈ 4.7068e18
This still fits in signed long, but its next power of two is 2^63.
Therefore `NTL::NextPowerOfTwo(f)` would need exponent 63. In the
tested build, NTL terminates with `NextPowerOfTwo: overflow`.
Thus, although `f` itself still fits in `long`, the mathematically
required scaling factor no longer does.
r = 55: ceil(2^r * roundErr) ≈ 9.4136e18
This already exceeds LONG_MAX, so the conversion performed by
`long f = std::ceil(...)` is out of range
The 55 UBSan output above directly matches that last threshold. In the separate tested r = 56 recovery-mode run, the same intermediate is larger still.
So there are two distinct failure modes:
- at
r = 54, f is representable but its next power of two is 2^63
- at the tested
r = 55, and likewise in the tested r = 56 recovery-mode run, the intermediate assigned to long f is already out of range
This is why the reproducer sees NextPowerOfTwo: overflow at 54, but an out-of-range floating-to-long conversion at 55.
The source itself already carries:
// VJS-FIXME: the computation of f and/or return value could overflow
which matches the behavior above.
Relevant source locations
Suggested direction
encodeScalingFactor() should resolve its context-derived default and then validate the complete scaling-factor computation before performing any potentially overflowing shift or floating-to-integer conversion.
In practice this likely means:
- primarily hardening
encodeScalingFactor() itself so that, after resolving the context-derived default, it validates both the precision * roundErr intermediate and the final rounded power-of-two scaling factor for representability by the helper's return type; and/or
- validating the builder-level CKKS precision range before
GenSecKey() reaches encodeScalingFactor()
Reported by Jiang Chao, Beijing University of Posts and Telecommunications
I would like to report a reproducible CKKS configuration bug in HElib's public context/key-generation path for high
precision(...)settings.On the checked-out HElib revision, a normal CKKS setup using:
or larger can reach an unrepresentable scaling-factor computation in
EncryptedArrayCx::encodeScalingFactor()duringSecKey::GenSecKey(), causing an NTL abort at54and undefined behavior at55on the tested build.Summary
In the checked-out implementation,
SecKey::ImportSecKey()computes the public encryption key's CKKS scaling factor with:pubEncrKey.ratFactor = pubEncrKey.noiseBound * getContext().getEA().getCx().encodeScalingFactor();and
encodeScalingFactor()defaults to:For high CKKS precision values, this can reach an unrepresentable scaling-factor computation before any explicit rejection of the context-derived default.
I reproduced the following behavior:
precision = 53: key generation succeeds normallyprecision = 54: key generation aborts withNextPowerOfTwo: overflowprecision = 55: UBSan reports an out-of-range conversion tolonginencodeScalingFactor()precision = 56reports the same out-of-range conversion class with a larger operandThis occurs in a normal public setup path:
without malformed serialized input or manual object corruption.
Environment
3e337a66a91a92d49de6a9505340826b0eb71081Clang 14.0.0NTLandGMPNTL_SP_NBITS = 60NTL_SP_BOUND = 1152921504606846976sizeof(long) = 8LONG_MAX = 9223372036854775807include/helib/EncryptedArray.h,include/helib/Context.h, andsrc/keys.cppwere clean when running this reproducer./tmp/helib-system-headers-v1contains onlyNTL/andgmp.hheaders used to prefer the system NTL 11 headers; it does not contain HElib headers.Representative build command:
Minimal reproduction
Control case:
Output:
Failing boundary:
Output:
UBSan boundary:
Output:
In a separate UBSan recovery-mode run,
precision = 56produced the same class of out-of-range conversion with a larger operand (1.88271e+19) and then continued to printkeygen_ok. I do not rely on that continued execution to establish the bug.Actual behavior
High
precision(...)values accepted by the CKKSContextBuildercan makeencodeScalingFactor()abort or trigger undefined behavior duringGenSecKey().At
precision = 54, the process aborts inNTL::NextPowerOfTwo.At
precision = 55, UBSan reports an out-of-range conversion tolonginencodeScalingFactor().With UBSan recovery enabled, later execution can continue and print
keygen_ok, but that post-UB continuation is not required to establish the bug.Expected behavior
If the current CKKS implementation does not support these high precision settings, the library should reject them explicitly before key generation reaches overflow or undefined behavior.
At minimum, a normal public
ContextBuilder<CKKS>plusGenSecKey()setup should not evaluate overflowing arithmetic or narrowing conversions in its internal scaling-factor computation.Impact
This affects a normal public configuration path for CKKS.
An honest caller can select a high precision through the documented builder API and trigger either:
I have not yet established a downstream wrong-result case from any key material produced after UBSan-recovery execution, but the key-generation path itself already reaches overflow and undefined behavior on a normal public configuration path.
Cause analysis
The checked-out implementation of
encodeScalingFactor()is:Two different quantities are called "precision" here:
ContextBuilder<CKKS>::precision(r)parameter is a bit precisionencodeScalingFactor()then converts that bit precision into the integer quantity2^rSo when
SecKey::ImportSecKey()uses the defaultencodeScalingFactor(), the context bit precisionris converted into:1L << alMod.getR()where
alMod.getR()is the CKKS bit precision configured by the public builder.The existing
precision < NTL_SP_BOUNDcheck does not validate that context-derived default value: it runs whileprecisionis still the-1sentinel, beforeprecisionis replaced with1L << alMod.getR().That validation-order gap is real, but it is not by itself a sufficient explanation for the observed
54/55boundary on this build. In the tested NTL configuration above, both2^54and2^55are still belowNTL_SP_BOUND.The more direct failure condition is that the helper never validates the full representability of:
or of the final rounded power-of-two scaling factor before performing the narrowing conversion to
longor forming a final power-of-two scaling factor that is not representable as a positivelong.For the reproducer parameters above:
m = 16384, sophi(m) = 8192scale_ = 10encodeRoundingError()returnsnoiseBoundForUniform(0.5, phim)noiseBoundForUniform()computesscale * sqrt(degBound / 3.0) * magBoundSo the default rounding-error bound here is:
That makes the key boundaries:
The
55UBSan output above directly matches that last threshold. In the separate testedr = 56recovery-mode run, the same intermediate is larger still.So there are two distinct failure modes:
r = 54,fis representable but its next power of two is2^63r = 55, and likewise in the testedr = 56recovery-mode run, the intermediate assigned tolong fis already out of rangeThis is why the reproducer sees
NextPowerOfTwo: overflowat54, but an out-of-range floating-to-longconversion at55.The source itself already carries:
// VJS-FIXME: the computation of f and/or return value could overflowwhich matches the behavior above.
Relevant source locations
include/helib/Context.hContext::getR()/Context::getPrecision(): lines 299-321Context::noiseBoundForUniform(...): lines 475-486ContextBuilderdefaultscale_ = 10: line 1081ContextBuilder<SCHEME>::precision(long): lines 1143-1146ContextBuilder<SCHEME>::scale(double): lines 1154-1157HElib/include/helib/Context.h
Lines 299 to 321 in 3e337a6
HElib/include/helib/Context.h
Lines 475 to 486 in 3e337a6
HElib/include/helib/Context.h
Line 1081 in 3e337a6
HElib/include/helib/Context.h
Lines 1143 to 1146 in 3e337a6
HElib/include/helib/Context.h
Lines 1154 to 1157 in 3e337a6
include/helib/EncryptedArray.hencodeRoundingError(...): lines 1287-1296encodeScalingFactor(...): lines 1299-1312HElib/include/helib/EncryptedArray.h
Lines 1287 to 1296 in 3e337a6
HElib/include/helib/EncryptedArray.h
Lines 1299 to 1312 in 3e337a6
src/keys.cppSecKey::ImportSecKey(...): lines 1099-1118ratFactorassignment usingencodeScalingFactor(): lines 1114-1117SecKey::GenSecKey(...): lines 1139-1155HElib/src/keys.cpp
Lines 1099 to 1118 in 3e337a6
HElib/src/keys.cpp
Lines 1139 to 1155 in 3e337a6
Suggested direction
encodeScalingFactor()should resolve its context-derived default and then validate the complete scaling-factor computation before performing any potentially overflowing shift or floating-to-integer conversion.In practice this likely means:
encodeScalingFactor()itself so that, after resolving the context-derived default, it validates both theprecision * roundErrintermediate and the final rounded power-of-two scaling factor for representability by the helper's return type; and/orGenSecKey()reachesencodeScalingFactor()Reported by Jiang Chao, Beijing University of Posts and Telecommunications