Skip to content

_ctypes breaks libffi's NULL-terminated elements invariant for complex types #148573

@sunmy2019

Description

@sunmy2019

Bug report

Bug description:

Summary

When a subclass of ctypes.c_double_complex (or c_float_complex, c_longdouble_complex) is created, the PyCSimpleType_init function in Modules/_ctypes/_ctypes.c allocates only sizeof(ffi_type *) bytes for stginfo->ffi_type_pointer.elements and copies a single pointer. This produces a non-NULL-terminated array, violating libffi's invariant that ffi_type.elements must be a NULL-terminated array of ffi_type *.

With a debug build of libffi, this triggers an assertion failure in ffi_prep_cif.

Reproduction

Build libffi with --enable-debug, then run:

LD_PRELOAD=/path/to/libffi_debug.so ./python -c "
import ctypes

@ctypes.CFUNCTYPE(ctypes.c_double_complex)
def cb():
    return 1 + 2j
"

Result:

ASSERTION FAILURE: a->type != FFI_TYPE_COMPLEX || (a->elements != NULL && a->elements[0] != NULL && a->elements[1] == NULL) at ../src/prep_cif.c:150
Aborted (core dumped)

Root Cause

In Modules/_ctypes/_ctypes.c, PyCSimpleType_init:

    if (!fmt->pffi_type->elements) {
        stginfo->ffi_type_pointer = *fmt->pffi_type;
    }
    else {
        const size_t els_size = sizeof(fmt->pffi_type->elements);
        stginfo->ffi_type_pointer.size = fmt->pffi_type->size;
        stginfo->ffi_type_pointer.alignment = fmt->pffi_type->alignment;
        stginfo->ffi_type_pointer.type = fmt->pffi_type->type;
        stginfo->ffi_type_pointer.elements = PyMem_Malloc(els_size);
        memcpy(stginfo->ffi_type_pointer.elements,
               fmt->pffi_type->elements, els_size);
    }

For most simple types, pffi_type->elements is NULL, so the if branch is taken and everything is fine. However, C complex types (ffi_type_complex_double, ffi_type_complex_float, etc.) have a non-NULL elements array:

static ffi_type *ffi_elements_complex_double[2] = { &ffi_type_double, NULL };
ffi_type ffi_type_complex_double = { sizeof(double complex), ..., FFI_TYPE_COMPLEX, ffi_elements_complex_double };

When the else branch runs for these types:

  • els_size is sizeof(ffi_type *) (8 bytes on 64-bit).
  • Only the first pointer (&ffi_type_double) is copied.
  • The second element (which should be NULL) is left as uninitialized heap memory.

This breaks libffi's invariant. libffi explicitly checks this in debug builds:

Impact

Any code path that passes the broken ffi_type to ffi_prep_cif or ffi_prep_cif_var is affected. This includes:

  1. Calling a C function with a subclassed complex type as return type or argument type.
  2. Creating a CFUNCTYPE callback with a subclassed complex return type.
  3. Embedding a subclassed complex type inside a Structure or Union and using it in a function call.

While current release builds of libffi may not crash immediately (because they happen not to read elements[1] for FFI_TYPE_COMPLEX), relying on this is depending on an implementation detail that is explicitly asserted in debug builds and may change in future libffi versions.

Affected Versions

CPython main branch (and likely all versions that support C complex types in _ctypes).

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions