Skip to content

[Bug report] ContextData_Parms and Modulus_Create2 still lack NULL output-pointer checks in SEAL v4.4.2 #753

Description

@CCYJ1014

Hi SEAL team,

I would like to report a SEAL C API output-pointer validation bug that remains partially unresolved in the current v4.4.2 release.

Summary

I initially identified three SEAL C exports in v4.4.0 that write through caller-provided output pointers or output buffers without validating them:

  • ContextData_Parms
  • Modulus_Create2
  • Modulus_ConstRatio

I dynamically reproduced process-level null-pointer dereferences for all three entry points on v4.4.0.

I then rebuilt and reran the same reproducer against v4.4.2 to determine the current status. The issue is only partially addressed:

  • ContextData_Parms: still missing validation for parms
  • Modulus_Create2: still missing validation for small_modulus
  • Modulus_ConstRatio: now validates ratio with IfNullRet(ratio, E_POINTER)

So the current report concerns the two output-pointer checks that still remain missing in v4.4.2. The Modulus_ConstRatio result is retained below as part of the original v4.4.0 findings and to document the v4.4.2 partial fix.

Passing NULL to either remaining output pointer still reaches a direct write through that pointer instead of returning E_POINTER.

Environment

Dynamic reproduction on v4.4.0:

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

Dynamic reproduction on v4.4.2:

  • 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

Status in v4.4.2

The three original v4.4.0 findings have the following status in v4.4.2.

ContextData_Parms — still affected

The function still validates only thisptr:

SEALContext::ContextData *cont_data = FromVoid<SEALContext::ContextData>(thisptr);
IfNullRet(cont_data, E_POINTER);

It then allocates an object and writes through parms without checking it:

EncryptionParameters *enc_params = new EncryptionParameters(cont_data->parms());
*parms = enc_params;

Modulus_Create2 — still affected

The function still validates only the source object:

Modulus *copypt = FromVoid<Modulus>(copy);
IfNullRet(copypt, E_POINTER);

It then writes through small_modulus without checking it:

Modulus *sm = new Modulus(*copypt);
*small_modulus = sm;

Modulus_ConstRatio — fixed in v4.4.2

The v4.4.2 implementation now includes:

IfNullRet(ratio, E_POINTER);

before the output copy. I also reran this case locally on v4.4.2, and it no longer crashes.

Minimal reproduction on v4.4.0 and v4.4.2

The reproducer below dynamically demonstrates the three conditions as they existed in v4.4.0, and it was rerun unchanged on v4.4.2.

The Modulus_ConstRatio mode is included for the historical v4.4.0 result and to confirm the v4.4.2 fix; only the first two modes still correspond to missing checks in v4.4.2.

#include "seal/c/contextdata.h"
#include "seal/c/modulus.h"
#include "seal/context.h"
#include "seal/encryptionparams.h"
#include "seal/modulus.h"

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

using namespace seal;

namespace
{
    int RunContextDataParmsNullOut()
    {
        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);
        auto *context_data = const_cast<SEALContext::ContextData *>(context.first_context_data().get());
        std::cout << "about to call ContextData_Parms with parms == NULL" << std::endl;
        return static_cast<int>(ContextData_Parms(context_data, nullptr));
    }

    int RunModulusCreate2NullOut()
    {
        Modulus modulus(257);
        std::cout << "about to call Modulus_Create2 with small_modulus == NULL" << std::endl;
        return static_cast<int>(Modulus_Create2(&modulus, nullptr));
    }

    int RunModulusConstRatioNullOut()
    {
        Modulus modulus(257);
        std::cout << "about to call Modulus_ConstRatio with ratio == NULL" << std::endl;
        auto result = Modulus_ConstRatio(&modulus, 3, nullptr);
        std::cout << "HRESULT=0x" << std::hex
                  << static_cast<unsigned long>(result) << std::dec << std::endl;
        return (result == E_POINTER) ? 0 : 1;
    }
} // namespace

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        return 2;
    }

    std::string mode = argv[1];
    if (mode == "contextdata-parms-null-out")
    {
        return Run
ContextDataParmsNullOut();
    }
    if (mode == "modulus-create2-null-out")
    {
        return RunModulusCreate2NullOut();
    }
    if (mode == "modulus-const-ratio-null-out")
    {
        return RunModulusConstRatioNullOut();
    }

    return 2;
}

Commands used:

./repro contextdata-parms-null-out
./repro modulus-create2-null-out
./repro modulus-const-ratio-null-out

Expected behavior

ContextData_Parms() and Modulus_Create2() should validate their required output pointers before allocation or assignment and return E_POINTER when those pointers are NULL.

This would match neighboring SEAL C exports such as ContextData_Qualifiers() and Modulus_Create1().

Modulus_ConstRatio() now follows this general pattern in v4.4.2 and is no longer included among the remaining affected exports.

Actual behavior on v4.4.0

On the tested v4.4.0 build, all three entry points terminated the process before returning an HRESULT.

Representative frames from my local v4.4.0 build for ContextData_Parms (local source line offsets omitted):

Program received signal SIGSEGV, Segmentation fault.
ContextData_Parms(...)
    at native/src/seal/c/contextdata.cpp
RunContextDataParmsNullOut()
    at repro.cpp

Representative frames from my local v4.4.0 build for Modulus_Create2 (local source line offsets omitted):

Program received signal SIGSEGV, Segmentation fault.
Modulus_Create2(...)
    at native/src/seal/c/modulus.cpp
RunModulusCreate2NullOut()
    at repro.cpp

Representative excerpt from my local ASan-instrumented v4.4.0 build for Modulus_ConstRatio (local source line offsets omitted):

ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
Modulus_ConstRatio(...)
    at native/src/seal/c/modulus.cpp
RunModulusConstRatioNullOut()
    at repro.cpp

Actual behavior on v4.4.2

On the tested v4.4.2 build:

  • ContextData_Parms still writes through parms == NULL and terminates the process
  • Modulus_Create2 still writes through small_modulus == NULL and terminates the process
  • Modulus_ConstRatio no longer crashes; it now rejects ratio == NULL

Representative v4.4.2 UBSan/ASan output for ContextData_Parms (local source line offsets omitted):

about to call ContextData_Parms with parms == NULL
runtime error: store to null pointer of type 'void *'
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 ContextData_Parms
    at native/src/seal/c/contextdata.cpp

Representative v4.4.2 UBSan/ASan output for Modulus_Create2 (local source line offsets omitted):

about to call Modulus_Create2 with small_modulus == NULL
runtime error: store to null pointer of type 'void *'
AddressSanitizer: SEGV on unknown address 0x000000000000
#0 Modulus_Create2
    at native/src/seal/c/modulus.cpp

For Modulus_ConstRatio on v4.4.2, the call now returns E_POINTER instead of crashing:

about to call Modulus_ConstRatio with ratio == NULL
HRESULT=0x80004003

Impact

The remaining confirmed impact in v4.4.2 is a process-level null-pointer dereference when a caller passes NULL for the required output pointer of ContextData_Parms() or Modulus_Create2().

This is primarily a C API robustness and availability issue. The caller must already be able to invoke the native export with an invalid output argument; serialized SEAL data alone does not trigger the condition.

In a managed/native boundary, plugin boundary, or another application path where output arguments can become malformed before reaching the C export, the condition can terminate the host process instead of returning a recoverable HRESULT.

Cause analysis

In v4.4.0, all three exports wrote through unchecked output arguments.

The first two missing checks remain present in v4.4.2. Modulus_ConstRatio() has since gained the corresponding IfNullRet(ratio, E_POINTER) guard.

ContextData_Parms — still affected in v4.4.2

SEAL_C_FUNC ContextData_Parms(void *thisptr, void **parms)
{
    SEALContext::ContextData *cont_data = FromVoid<SEALContext::ContextData>(thisptr);
    IfNullRet(cont_data, E_POINTER);

    try
    {
        EncryptionParameters *enc_params = new EncryptionParameters(cont_data->parms());
        *parms = enc_params;
        return S_OK;
    }
    SEAL_C_CATCH_ALL
}

Modulus_Create2 — still affected in v4.4.2

SEAL_C_FUNC Modulus_Create2(void *copy, void **small_modulus)
{
    Modulus *copypt = FromVoid<Modulus>(copy);
    IfNullRet(copypt, E_POINTER);

    try
    {
        Modulus *sm = new Modulus(*copypt);
        *small_modulus = sm;
        return S_OK;
    }
    SEAL_C_CATCH_ALL
}

Modulus_ConstRatio — affected in v4.4.0, fixed in v4.4.2

In v4.4.0, the function copied to ratio without validating it:

if (length != 3)
{
    return E_INVALIDARG;
}

auto ratio_array = sm->const_ratio();
copy(ratio_array.begin(), ratio_array.end(), ratio);

In v4.4.2, it now performs:

IfNullRet(ratio, E_POINTER);

if (length != 3)
{
    return E_INVALIDARG;
}

SEAL_C_CATCH_ALL can translate C++ exceptions into HRESULT values, but it does not convert a Linux SIGSEGV caused by writing through a null pointer into a recoverable return code. Modulus_ConstRatio() previously performed its output copy outside any such exception-translation block, but that entry point is now protected by the explicit null check in v4.4.2.

Relevant source locations

Original v4.4.0 findings:

Current v4.4.2 status:

Suggested direction

The remaining missing checks in v4.4.2 appear straightforward:

  • add IfNullRet(parms, E_POINTER) to ContextData_Parms() before allocating the new EncryptionParameters
  • add IfNullRet(small_modulus, E_POINTER) to Modulus_Create2() before allocating the new Modulus

Question

Should the two remaining exports, ContextData_Parms() and Modulus_Create2(), also return E_POINTER for NULL output pointers,
consistent with the checks already used by neighboring exports and now by Modulus_ConstRatio() in v4.4.2?

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