Hi OpenFHE team,
I would like to report a possible memory-safety issue in the BinFHE decryption path and ask whether malformed ciphertext length is supposed to be validated at this API boundary.
Summary
BinFHEContext::Decrypt() forwards a malformed LWECiphertext into LWEEncryptionScheme::Decrypt() without verifying that the ciphertext length matches the secret-key length. A one-element a vector is enough to trigger an ASan heap-buffer-overflow in the decryption loop.
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 ciphertext and then replaces its a vector with a one-element vector:
#include "binfhecontext.h"
#include "lwe-ciphertext.h"
using namespace lbcrypto;
int main() {
BinFHEContext cc;
cc.GenerateBinFHEContext(TOY);
auto sk = cc.KeyGen();
auto ct = cc.Encrypt(sk, 1);
NativeVector shortA(1, ct->GetModulus());
shortA[0] = ct->GetA()[0];
auto malformed =
std::make_shared<LWECiphertextImpl>(std::move(shortA), ct->GetB(), ct->GetptModulus());
LWEPlaintext result = 0;
cc.Decrypt(sk, malformed, &result);
return 0;
}
Expected behavior
Decrypt() should reject the malformed ciphertext and report a dimension mismatch before doing arithmetic on ct->GetA().
Actual behavior
The process crashes with an ASan heap-buffer-overflow.
Relevant excerpt:
==...==ERROR: AddressSanitizer: heap-buffer-overflow
#0 ... NativeIntegerT<unsigned long>::ModMulFast(...) .../ubintnat.h:1353
#1 ... lbcrypto::LWEEncryptionScheme::Decrypt(...) .../lwe-pke.cpp:189
#2 ... lbcrypto::BinFHEContext::Decrypt(...) .../binfhecontext.cpp:269
Cause analysis
BinFHEContext::Decrypt() only checks for null pointers and then forwards the ciphertext to LWEEncryptionScheme::Decrypt().
Inside LWEEncryptionScheme::Decrypt(), the loop bound is taken from the secret-key length:
const auto& a = ct->GetA();
auto s = sk->GetElement();
uint32_t n = s.GetLength();
...
for (uint32_t i = 0; i < n; ++i) {
inner += a[i].ModMulFast(s[i], q, mu);
}
So once ct->GetLength() < sk->GetLength(), the loop reads past the end of ct->GetA().
Relevant source locations
src/binfhe/lib/binfhecontext.cppBinFHEContext::Decrypt(): lines 263-269- GitHub link: https://github.com/openfheorg/openfhe-development/blob/main/src/binfhe/lib/binfhecontext.cpp#L263-L269
src/binfhe/lib/lwe-pke.cppLWEEncryptionScheme::Decrypt(): lines 165-203- OOB read in the loop: line 189
- GitHub link: https://github.com/openfheorg/openfhe-development/blob/main/src/binfhe/lib/lwe-pke.cpp#L165-L203
Suggested direction
It looks reasonable to validate:
ct->GetLength() == sk->GetLength()- and possibly modulus compatibility between
ct,sk, and the activeLWECryptoParams
before entering the decryption loop.
Question
Is BinFHEContext::Decrypt() expected to reject malformed ciphertext dimensions, or is that currently considered a caller-side precondition only?
Reported by Jiang Chao, Beijing University of Posts and Telecommunications