Hi OpenFHE team,
I would like to report an input-validation issue in the BinFHE multi-input gate API that can result in undefined behavior and process crashes.
Summary
BinFHEContext::EvalBinGate(BINGATE, const std::vector<LWECiphertext>&, ...) forwards the input vector to BinFHEScheme::EvalBinGate() without checking:
- that the vector has the exact arity required by the selected gate
- that no element in the vector is null
In particular, for AND3, OR3, AND4, OR4, and MAJORITY, an empty vector reaches the ctvector[0] access before any length validation occurs.
I reproduced three distinct malformed-input behaviors locally under ASan/UBSan on OpenFHE 1.5.1:
EvalBinGate(AND3, {})crashes on an empty vectorEvalBinGate(AND3, {validCt1, nullptr, validCt2})crashes even though the vector length is correctEvalBinGate(AND3, {validCt1, validCt2})is accepted and returns a ciphertext instead of rejecting the unsupported arity
Environment
- OpenFHE version:
1.5.1 - Compiler:
clang 14.0.0 - Sanitizers:
AddressSanitizer,UndefinedBehaviorSanitizer
Public issue check
As of July 30, 2026, I found a related earlier public null-input report:
- OpenFHE Discourse topic
Boolean Scheme Decrypt Error(/t/boolean-scheme-decrypt-error/1704) - linked GitHub issue
#907 Add error handling
That Discourse follow-up generically lists cc.EvalBinGate() among APIs that may crash on nullptr inputs, so I do not think this report is completely unrelated to the earlier bug family.
However, I did not find an exact public report for the vector overload crashing on:
- empty input vectors
- missing arity checks for
AND3/OR3/AND4/OR4/MAJORITY - null elements inside the vector overload specifically
I also reviewed the later OpenFHE fixes that appear to be related to the earlier report:
- PR
#939added explicitnullptrchecks to the two-ciphertextEvalBinGate(ct1, ct2)path, but in the vector overload it only added aparams == nullptrcheck - PR
#1078, which closed#907, added null checks only toLWEEncryptionScheme::Decrypt()
So this looks more like a related but still-unfixed vector-overload variant.
Minimal reproduction
The reproducer below passes an empty ciphertext vector to the multi-input gate overload:
#include "binfhecontext.h"
using namespace lbcrypto;
int main() {
BinFHEContext cc;
cc.GenerateBinFHEContext(TOY);
auto sk = cc.KeyGen();
cc.BTKeyGen(sk);
std::vector<LWECiphertext> cts;
auto out = cc.EvalBinGate(AND3, cts);
(void)out;
return 0;
}
To isolate null-element handling from arity validation, I also reproduced the issue with a correctly sized AND3 vector whose second element was null:
auto ct0 = cc.Encrypt(sk, 0);
auto ct1 = cc.Encrypt(sk, 1);
std::vector<LWECiphertext> cts = {ct1, LWECiphertext{}, ct0};
auto out = cc.EvalBinGate(AND3, cts);
To isolate the arity-validation problem from null handling, I also reproduced that AND3 accepts a two-element vector whose elements are both valid ciphertexts:
auto ct0 = cc.Encrypt(sk, 0);
auto ct1 = cc.Encrypt(sk, 1);
std::vector<LWECiphertext> cts = {ct1, ct0};
auto out = cc.EvalBinGate(AND3, cts);
Expected behavior
The vector overload should reject malformed inputs with an OpenFHE exception before performing any vector indexing or ciphertext dereference. At minimum, I would expect:
- exact arity validation for
AND3,OR3,AND4,OR4, andMAJORITY - null-element validation
CMUX already has a size-3 check in the current implementation, so I am not including it in the missing-arity portion of this report.
Actual behavior
For the empty-vector case, UBSan reports invalid access through std::vector::operator[], followed by a crash in BinFHEScheme::EvalBinGate():
runtime error: reference binding to null pointer of type 'const std::shared_ptr<lbcrypto::LWECiphertextImpl>'
#0 ... std::vector<...>::operator[](...) .../stl_vector.h:1143
#1 ... lbcrypto::BinFHEScheme::EvalBinGate(...) .../binfhe-base-scheme.cpp:139
For the correct-arity null-element case, UBSan reports a null member call and ASan ends with a null dereference:
runtime error: member call on null pointer of type 'lbcrypto::LWECiphertextImpl'
#0 ... lbcrypto::BinFHEScheme::EvalBinGate(...) .../binfhe-base-scheme.cpp:141
...
==...==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 ... lbcrypto::LWECiphertextImpl::GetModulus() const .../lwe-ciphertext.h:86
#1 ... lbcrypto::BinFHEScheme::EvalBinGate(...) .../binfhe-base-scheme.cpp:141
For the wrong-arity case, no exception is thrown: EvalBinGate(AND3, {validCt1, validCt2}) completes and returns a non-null ciphertext instead of rejecting the unsupported input length.
Cause analysis
The vector overload in BinFHEContext does not perform any validation:
LWECiphertext BinFHEContext::EvalBinGate(const BINGATE gate,
const std::vector<LWECiphertext>& ctvector,
bool extended) const {
return m_binfhescheme->EvalBinGate(m_params, gate, m_BTKey, ctvector, extended);
}
Inside BinFHEScheme::EvalBinGate(), in the branch handling AND3, OR3, AND4, OR4, and MAJORITY, the implementation does not validate the vector length or whether its elements are non-null before the first ctvector[0] dereference:
auto ct = (Q == ctvector[0]->GetModulus())
? LWEscheme->SwitchCTtoqn(LWEParams, EK.KSkey, ctvector[0])
: std::make_shared<LWECiphertextImpl>(*ctvector[0]);
...
auto p = ctvector[0]->GetptModulus();
So an empty vector is unsafe, and any call that reaches this line with a null first element is unsafe:
ctvector.empty()ctvector[0] == nullptr
The same code path then iterates over the remaining elements and dereferences ctvector[i] in the addition loop without checking whether any later element is null:
for (uint32_t i = 1; i < length; ++i) {
LWEscheme->EvalAddEq(ct, (Q == ctvector[i]->GetModulus()) ? LWEscheme->SwitchCTtoqn(LWEParams, EK.KSkey, ctvector[i]) : ctvector[i]);
}
So a correctly sized vector such as {validCt1, nullptr, validCt2} still reaches an unsafe dereference.
The same function also does not appear to enforce the documented arity for AND3, OR3, AND4, OR4, or MAJORITY. A two-element AND3 vector with both elements valid is accepted and processed instead of being rejected up front.
By contrast, CMUX does already check length != 3 before proceeding, so the missing-arity part of this report applies to the other multi-input gates, not to CMUX.
Relevant source locations
src/binfhe/lib/binfhecontext.cppBinFHEContext::EvalBinGate(vector overload): lines 283-285- GitHub link: https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/binfhe/lib/binfhecontext.cpp#L283-L285
src/binfhe/lib/binfhe-base-scheme.cppBinFHEScheme::EvalBinGate(vector overload): lines 121-173- First unsafe dereference: line 139
- Null later-element dereference in the accumulation loop: line 141
- GitHub link: https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/binfhe/lib/binfhe-base-scheme.cpp#L121-L173
src/binfhe/include/binfhecontext.h- documented vector-gate API: lines 290-297
- GitHub link: https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/binfhe/include/binfhecontext.h#L290-L297
src/binfhe/include/binfhe-base-scheme.h- documented supported vector gates: lines 96-107
- GitHub link: https://github.com/openfheorg/openfhe-development/blob/v1.5.1/src/binfhe/include/binfhe-base-scheme.h#L96-L107
Suggested direction
It looks reasonable to validate, before any dereference:
- required vector size for
AND3,OR3,AND4,OR4, andMAJORITY ctvector[i] != nullptrfor every element
It might also be worth keeping the vector overload consistent with the two-input overload, which already checks for null ciphertexts explicitly after the earlier null-input fixes.
Question
Is malformed multi-input gate input supposed to be rejected explicitly at the EvalBinGate(vector) boundary, or is that currently treated as an unchecked caller-side precondition?
Reported by Jiang Chao, Beijing University of Posts and Telecommunications