Hi OpenFHE team,
I would like to report an input-validation issue in BinFHEContext::SwitchCTtoqn() that can lead to an out-of-bounds read inside KeySwitch(), and ask whether the current validation logic is intentional.
Summary
BinFHEContext::SwitchCTtoqn() uses a malformed-input guard that only rejects the ciphertext when both the dimension and modulus are wrong. As a result:
- wrong dimension + expected modulus
Qpasses - expected dimension
N+ wrong modulus also passes
In the reproducer below, a ciphertext with the expected large modulus Q but a one-element a vector passes the guard and later crashes inside KeySwitch().
I reproduced this locally under ASan/UBSan on OpenFHE 1.5.1.
Environment
- OpenFHE version:
1.5.1 - Compiler:
clang 14.0.0 - Sanitizers:
AddressSanitizer,UndefinedBehaviorSanitizer
Minimal reproduction
The reproducer below starts from a valid large-dimension ciphertext, shrinks its a vector to length 1, and then calls SwitchCTtoqn():
#include "binfhecontext.h"
#include "lwe-ciphertext.h"
using namespace lbcrypto;
int main() {
BinFHEContext cc;
cc.GenerateBinFHEContext(TOY);
auto sk = cc.KeyGen();
cc.BTKeyGen(sk);
auto pk = cc.PubKeyGen(sk);
auto ctLarge = cc.Encrypt(pk, 1, LARGE_DIM);
NativeVector shortA(1, ctLarge->GetModulus());
shortA[0] = ctLarge->GetA()[0];
auto malformed =
std::make_shared<LWECiphertextImpl>(std::move(shortA), ctLarge->GetB(), ctLarge->GetptModulus());
auto out = cc.SwitchCTtoqn(cc.GetSwitchKey(), malformed);
(void)out;
return 0;
}
Expected behavior
SwitchCTtoqn() should reject the ciphertext if either its dimension is not N or its modulus is not Q.
In this reproducer, it should be rejected because its dimension is 1 rather than N.
Actual behavior
The malformed ciphertext passes the front-end guard and later crashes with an ASan heap-buffer-overflow inside KeySwitch().
Relevant excerpt:
==...==ERROR: AddressSanitizer: heap-buffer-overflow
#0 ... NativeIntegerT<unsigned long>::ConvertToInt(...) .../ubintnat.h:1652
#1 ... lbcrypto::LWEEncryptionScheme::KeySwitch(...) .../lwe-pke.cpp:336
#2 ... lbcrypto::LWEEncryptionScheme::SwitchCTtoqn(...) .../lwe-pke.cpp:158
#3 ... lbcrypto::BinFHEContext::SwitchCTtoqn(...) .../binfhecontext.cpp:260
The sanitizer line numbers above refer to my local instrumented source tree; the stable v1.5.1 source links are listed below.
Cause analysis
The current guard is:
if ((ct->GetLength() != LWEParams->GetN()) && (ct->GetModulus() != LWEParams->GetQ()))
OPENFHE_THROW("ciphertext dimension and modulus are not large N and Q");
This only throws when both invariants are violated at the same time.
Therefore, either a wrong dimension with modulus Q, or dimension N with the wrong modulus, can pass the guard.
The reproducer exercises the first case: wrong dimension with the expected modulus Q.
The modulus-switching step preserves the ciphertext vector length, so the one-element input reaches KeySwitch() as a one-element ciphertext.
KeySwitch() then uses params->GetN() as the fixed loop bound rather than the actual ciphertext length and reaches:
NativeInteger::Integer atmp(ctQN->GetA()[i].ConvertToInt());
inside the for (uint32_t i = 0; i < N; ++i) loop in KeySwitch().
Relevant source locations
src/binfhe/lib/binfhecontext.cppBinFHEContext::SwitchCTtoqn(): lines 228-237- Incorrect guard: line 234
- GitHub link: https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/binfhe/lib/binfhecontext.cpp#L228-L237
src/binfhe/lib/lwe-pke.cppLWEEncryptionScheme::SwitchCTtoqn(): lines 136-145LWEEncryptionScheme::ModSwitch(): lines 216-224LWEEncryptionScheme::KeySwitch(): lines 292-315- Crash-relevant read: line 304
- GitHub links:
Suggested direction
It looks like the front-end guard should use || rather than &&, for example:
if ((ct->GetLength() != LWEParams->GetN()) ||
(ct->GetModulus() != LWEParams->GetQ()))
OPENFHE_THROW("ciphertext must have large dimension N and modulus Q");
It may also be worth enforcing the dimension invariant in the lower-level LWEEncryptionScheme::SwitchCTtoqn() path or immediately before the fixed-N indexing in KeySwitch().
Question
Is the current && guard intentional here, or would you consider this a bug in the input validation logic?
Reported by Jiang Chao, Beijing University of Posts and Telecommunications