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
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.2release.Summary
I originally found this crash pattern on SEAL
v4.4.0and then rebuilt and reran the same reproducer against SEALv4.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 isNULL:EncParams_SetCoeffModulusEvaluator_AddManyEvaluator_MultiplyManyKSwitchKeys_AddKeyListOn the tested
v4.4.2build, all four entry points still terminate the process when the handle array itself is non-null but contains aNULLelement.The shared pattern is:
Environment
Dynamic reproduction on the current release:
v4.4.2cbff8895f2307241e6fcc91fd791457227ffb2a2Clang 14.0.0AddressSanitizer,UndefinedBehaviorSanitizerOriginal discovery:
v4.4.004d53b99ce745efc26bb4965be609b9894755227Minimal reproduction
The reproducer below uses valid native SEAL objects of the exact types expected by the C exports and independently injects a
NULLelement into each handle array:Commands used:
Expected behavior
If any element at an index below
lengthorcountisNULL, the export should reject the call before dereferencing that element and returnE_POINTER.Actual behavior on
v4.4.2On the tested
v4.4.2build, 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:Evaluator_AddMany:Evaluator_MultiplyMany:KSwitchKeys_AddKeyList:Impact
The confirmed impact is a process-level null-pointer dereference when a caller passes a non-null handle array containing at least one
NULLelement.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_SetCoeffModulusEvaluator_AddManyEvaluator_MultiplyManyKSwitchKeys_AddKeyListIn
EncParams_SetCoeffModulus(),Evaluator_AddMany(), andEvaluator_MultiplyMany(), the per-element dereferences occur before thetryblock.KSwitchKeys_AddKeyList()performs its dereference inside thetryblock, but a native null-pointer dereference raisesSIGSEGV; it is not a C++ exception thatSEAL_C_CATCH_ALLcan translate into an HRESULT.There is also a state-management concern in
KSwitchKeys_AddKeyList(): the function appends a new key-list container withkeys->data().emplace_back()before validating any element in the caller-supplied array.Relevant source locations
EncParams_SetCoeffModulus():https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/encryptionparameters.cpp#L115-L134
Evaluator_AddMany():https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/evaluator.cpp#L91-L113
Evaluator_MultiplyMany():https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/evaluator.cpp#L193-L219
KSwitchKeys_AddKeyList():https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/kswitchkeys.cpp#L152-L177
IfNullRet:https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/stdafx.h#L20-L26
E_POINTER/SEAL_C_CATCH_ALL:https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/defines.h#L41-L42
https://github.com/microsoft/SEAL/blob/v4.4.2/native/src/seal/c/defines.h#L73-L96
Suggested direction
Before copying any objects or mutating the destination, scan all entries below
length/countand returnE_POINTERif any handle isNULL.In particular:
coeffs,encrypteds, andkey_listbefore dereferencing itvector<Modulus>/vector<Ciphertext>staging objectsKSwitchKeys_AddKeyList(), complete validation beforekeys->data().emplace_back()so a rejected call cannot leave a partially mutated destination objectQuestion
Should these exports return
E_POINTERwhen a required handle-array element isNULL, instead of terminating the process?Reported by Jiang Chao, Beijing University of Posts and Telecommunications