[Bug report] BinFHE EvalBinGate(vector) lacks arity/null checks and crashes on malformed vectors

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:

  1. EvalBinGate(AND3, {}) crashes on an empty vector
  2. EvalBinGate(AND3, {validCt1, nullptr, validCt2}) crashes even though the vector length is correct
  3. EvalBinGate(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 #939 added explicit nullptr checks to the two-ciphertext EvalBinGate(ct1, ct2) path, but in the vector overload it only added a params == nullptr check
  • PR #1078, which closed #907, added null checks only to LWEEncryptionScheme::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, and MAJORITY
  • 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

Suggested direction

It looks reasonable to validate, before any dereference:

  • required vector size for AND3, OR3, AND4, OR4, and MAJORITY
  • ctvector[i] != nullptr for 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

Again, our focus is on the honest-but-curious threat model. I’ve added this item to issue [Bug report] BinFHE EvalBinGate(vector) lacks arity/null checks and crashes on malformed vectors as it seems to be easy to fix.