Skip to content

[Bug report] Four SEAL C API handle-array exports still crash on NULL elements in SEAL v4.4.2 #754

Description

@CCYJ1014

Hi SEAL team,

I would like to report a SEAL C API per-element handle validation bug that remains present in the current v4.4.2 release.

Summary

I originally found this crash pattern on SEAL v4.4.0 and then rebuilt and reran the same reproducer against SEAL v4.4.2.

These four public C exports still validate only the outer void ** handle array pointer and still dereference individual elements without checking whether any element is NULL:

  • EncParams_SetCoeffModulus
  • Evaluator_AddMany
  • Evaluator_MultiplyMany
  • KSwitchKeys_AddKeyList

On the tested v4.4.2 build, all four entry points still terminate the process when the handle array itself is non-null but contains a NULL element.

The shared pattern is:

outer handle-array pointer checked
  -> NULL element not checked
  -> wrapper dereferences that element
  -> SIGSEGV instead of HRESULT

Environment

Dynamic reproduction on the current release:

  • SEAL release/tag: v4.4.2
  • Tested revision: cbff8895f2307241e6fcc91fd791457227ffb2a2
  • OS: Linux x86_64
  • Compiler: Clang 14.0.0
  • Sanitizer instrumentation used during path confirmation: AddressSanitizer, UndefinedBehaviorSanitizer

Original discovery:

  • SEAL release/tag: v4.4.0
  • Tested revision: 04d53b99ce745efc26bb4965be609b9894755227

Minimal reproduction

The reproducer below uses valid native SEAL objects of the exact types expected by the C exports and independently injects a NULL element into each handle array:

#include "seal/batchencoder.h"
#include "seal/c/encryptionparameters.h"
#include "seal/c/evaluator.h"
#include "seal/c/kswitchkeys.h"
#include "seal/encryptionparams.h"
#include "seal/encryptor.h"
#include "seal/evaluator.h"
#include "seal/keygenerator.h"
#include "seal/relinkeys.h"

#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

using namespace seal;

namespace
{
    int RunEncParamsNullCoeff()
    {
        EncryptionParameters parms(scheme_type::bfv);
        void *coeffs[1] = { nullptr };
        std::cout << "about to call EncParams_SetCoeffModulus with coeffs[0] == NULL" << std::endl;
        return static_cast<int>(EncParams_SetCoeffModulus(&parms, 1, coeffs));
    }

    int RunKSwitchKeysNullKey()
    {
        KSwitchKeys keys;
        void *key_list[1] = { nullptr };
        std::cout << "about to call KSwitchKeys_AddKeyList with key_list[0] == NULL" << std::endl;
        return static_cast<int>(KSwitchKeys_AddKeyList(&keys, 1, key_list));
    }

    int RunEvaluatorAddManyNullCipher()
    {
        EncryptionParameters parms(scheme_type::bfv);
        parms.set_poly_modulus_degree(4096);
        parms.set_coeff_modulus(CoeffModulus::BFVDefault(4096));
        parms.set_plain_modulus(PlainModulus::Batching(4096, 20));

        SEALContext context(parms);
        KeyGenerator keygen(context);
        PublicKey public_key;
        keygen.create_public_key(public_key);

        Encryptor encryptor(context, public_key);
        Evaluator evaluator(context);
        BatchEncoder encoder(context);

        std::vector<std::uint64_t> slots(encoder.slot_count(), 1);
        Plaintext plain;
        encoder.encode(slots, plain);

        Ciphertext c0;
        encryptor.encrypt(plain, c0);
        Ciphertext destination;

        void *ciphertexts[2] = { &c0, nullptr };
        std::cout << "about to call Evaluator_AddMany with ciphertexts[1] == NULL" << std::endl;
        return static_cast<int>(Evaluator_AddMany(&evaluator, 2, ciphertexts, &destination));
    }

    int RunEvaluatorMultiplyManyNullCipher()
    {
        EncryptionParameters parms(scheme_type::bfv);
        parms.set_poly_modulus_degree(4096);
        parms.set_coeff_modulus(CoeffModulus::BFVDefault(4096));
        parms.set_plain_modulus(PlainModulus::Batching(4096, 20));

        SEALContext context(parms);
        KeyGenerator keygen(context);
        PublicKey public_key;
        keygen.create_public_key(public_key);
        RelinKeys relin_keys;
        keygen.create_relin_keys(relin_keys);

        Encryptor encryptor(context, public_key);
        Evaluator evaluator(context);
        BatchEncoder encoder(context);

        std::vector<std::uint64_t> slots(encoder.slot_count(), 1);
        Plaintext plain;
        encoder.encode(slots, plain);

        Ciphertext c0;
        encryptor.encrypt(plain, c0);
        Ciphertext destination;

        void *ciphertexts[2] = { &c0, nullptr };
        std::cout << "about to call Evaluator_MultiplyMany with ciphertexts[1] == NULL" << std::endl;
        return static_cast<int>(
            Evaluator_MultiplyMany(&evaluator, 2, ciphertexts, &relin_keys, &destination, nullptr));
    }
} // namespace

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        std::cerr << "usage: " << argv[0]
                  << " <encparams-null-coeff|kswitchkeys-null-key|evaluator-addmany-null|evaluator-multiplymany-null>"
                  << std::endl;
        return 2;
    }

    std::string mode = argv[1];
    if (mode == "encparams-null-coeff")
    {
        return RunEncParamsNullCoeff();
    }
    if (mode == "kswitchkeys-null-key")
    {
        return RunKSwitchKeysNullKey();
    }
    if (mode == "evaluator-addmany-null")
    {
        return RunEvaluatorAddManyNullCipher();
    }
    if (mode == "evaluator-multiplymany-null")
    {
        return RunEvaluatorMultiplyManyNullCipher();
    }

    std::cerr << "unknown mode: " << mode << std::endl;
    return 2;
}

Commands used:

./repro encparams-null-coeff
./repro kswitchkeys-null-key
./repro evaluator-addmany-null
./repro evaluator-multiplymany-null

Expected behavior

If any element at an index below length or count is NULL, the export should reject the call before dereferencing that element and return E_POINTER.

Actual behavior on v4.4.2

On the tested v4.4.2 build, all four entry points terminate the process instead of returning an HRESULT.

Representative sanitizer excerpts are abbreviated below; addresses, unrelated frames, and local source line offsets have been omitted.

EncParams_SetCoeffModulus:

about to call EncParams_SetCoeffModulus with coeffs[0] == NULL
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 EncParams_SetCoeffModulus
    at native/src/seal/c/encryptionparameters.cpp

Evaluator_AddMany:

about to call Evaluator_AddMany with ciphertexts[1] == NULL
runtime error: reference binding to null pointer of type 'seal::Ciphertext'
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 Evaluator_AddMany
    at native/src/seal/c/evaluator.cpp

Evaluator_MultiplyMany:

about to call Evaluator_MultiplyMany with ciphertexts[1] == NULL
runtime error: reference binding to null pointer of type 'seal::Ciphertext'
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 Evaluator_MultiplyMany
    at native/src/seal/c/evaluator.cpp

KSwitchKeys_AddKeyList:

about to call KSwitchKeys_AddKeyList with key_list[0] == NULL
runtime error: reference binding to null pointer of type 'const seal::PublicKey'
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 KSwitchKeys_AddKeyList
    at native/src/seal/c/kswitchkeys.cpp

Impact

The confirmed impact is a process-level null-pointer dereference when a caller passes a non-null handle array containing at least one NULL element.

In a managed/native boundary, plugin boundary, or another application path where arrays of native handles can be incompletely initialized or incorrectly marshaled, the condition can terminate the host process instead of returning a recoverable HRESULT.

Cause analysis

The C export layer is designed to translate C++ exceptions into HRESULTs via SEAL_C_CATCH_ALL, but these four wrappers do not validate individual array elements before dereferencing them.

EncParams_SetCoeffModulus

EncryptionParameters *params = FromVoid<EncryptionParameters>(thisptr);
IfNullRet(params, E_POINTER);
IfNullRet(coeffs, E_POINTER);

Modulus **coeff_array = reinterpret_cast<Modulus **>(coeffs);
vector<Modulus> coefficients(length);

for (uint64_t i = 0; i < length; i++)
{
    coefficients[i] = *coeff_array[i];
}

Evaluator_AddMany

Evaluator *eval = FromVoid<Evaluator>(thisptr);
IfNullRet(eval, E_POINTER);
IfNullRet(encrypteds, E_POINTER);
Ciphertext *destination_ptr = FromVoid<Ciphertext>(destination);
IfNullRet(destination_ptr, E_POINTER);

Ciphertext **encrypteds_pp = reinterpret_cast<Ciphertext **>(encrypteds);
vector<Ciphertext> encrypteds_vec;

encrypteds_vec.reserve(count);
for (uint64_t i = 0; i < count; i++)
{
    encrypteds_vec.emplace_back(*encrypteds_pp[i]);
}

Evaluator_MultiplyMany

Evaluator *eval = FromVoid<Evaluator>(thisptr);
IfNullRet(eval, E_POINTER);
IfNullRet(encrypteds, E_POINTER);
RelinKeys *relin_keys_ptr = FromVoid<RelinKeys>(relin_keys);
IfNullRet(relin_keys_ptr, E_POINTER);
Ciphertext *destination_ptr = FromVoid<Ciphertext>(destination);
IfNullRet(destination_ptr, E_POINTER);

Ciphertext **encrypteds_pp = reinterpret_cast<Ciphertext **>(encrypteds);
vector<Ciphertext> encrypteds_vec;

encrypteds_vec.reserve(count);
for (uint64_t i = 0; i < count; i++)
{
    encrypteds_vec.emplace_back(*encrypteds_pp[i]);
}

KSwitchKeys_AddKeyList

KSwitchKeys *keys = FromVoid<KSwitchKeys>(thisptr);
IfNullRet(keys, E_POINTER);
IfNullRet(key_list, E_POINTER);

try
{
    PublicKey **key = reinterpret_cast<PublicKey **>(key_list);

    // Don't resize, only reserve
    keys->data().emplace_back();
    keys->data().back().reserve(count);

    for (uint64_t i = 0; i < count; i++)
    {
        PublicKey *pkey = key[i];
        PublicKey new_pkey(ph::Create(keys->pool()));
        new_pkey = *pkey;
        keys->data().back().emplace_back(std::move(new_pkey));
    }

In EncParams_SetCoeffModulus(), Evaluator_AddMany(), and Evaluator_MultiplyMany(), the per-element dereferences occur before the try block.

KSwitchKeys_AddKeyList() performs its dereference inside the try block, but a native null-pointer dereference raises SIGSEGV; it is not a C++ exception that SEAL_C_CATCH_ALL can translate into an HRESULT.

There is also a state-management concern in KSwitchKeys_AddKeyList(): the function appends a new key-list container with keys->data().emplace_back() before validating any element in the caller-supplied array.

Relevant source locations

Suggested direction

Before copying any objects or mutating the destination, scan all entries below length / count and return E_POINTER if any handle is NULL.

In particular:

  • prevalidate every element in coeffs, encrypteds, and key_list before dereferencing it
  • perform that validation before allocating or populating the local vector<Modulus> / vector<Ciphertext> staging objects
  • in KSwitchKeys_AddKeyList(), complete validation before keys->data().emplace_back() so a rejected call cannot leave a partially mutated destination object

Question

Should these exports return E_POINTER when a required handle-array element is NULL, instead of terminating the process?

Reported by Jiang Chao, Beijing University of Posts and Telecommunications

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions