Why am I receiving Segment Fault on C extension method declaration?

I am trying to create the __reduce__ method for a C extension type for Python I implemented so it become pickable. I have already done it with other types, but for some reason in this case I am receiving a Segment Fault.

Here is the minimal reproducible example:

main.c

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct
{
    PyObject_HEAD unsigned char attr1;
    unsigned char attr2;
    unsigned char attr3;
} SomeObject;

static int SomeObject__init(SomeObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"attr1", "attr2", "attr3", NULL};
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "bbb", kwlist, &self->attr1,
                                     &self->attr2, &self->attr3))
    {
        return -1;
    }

    return 0;
}

static PyObject *SomeObject__reduce(SomeObject *self, PyObject *Py_UNUSED(ignored))
{
    return Py_BuildValue("O(BBB)N", Py_TYPE(self), self->attr1, self->attr2, self->attr3,
                         Py_None);
}

static PyMethodDef SomeObject__methods[] = {
    {
        .ml_name = "__reduce__",
        .ml_meth = (PyCFunction)SomeObject__reduce,
        .ml_flags = METH_NOARGS,
    },
};

static PyTypeObject SomeType = {
    PyVarObject_HEAD_INIT(NULL, 0)
        .tp_name = "somemodule.SomeObject",
    .tp_basicsize = sizeof(SomeObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_methods = SomeObject__methods,
    .tp_init = (initproc)SomeObject__init,
    .tp_new = PyType_GenericNew,
};

static PyModuleDef somemodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "somemodule",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_somemodule(void)
{
    if (PyType_Ready(&SomeType) < 0)
        return NULL;

    PyObject *m = PyModule_Create(&somemodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SomeType);
    if (PyModule_AddObject(m, "Some", (PyObject *)&SomeType) < 0)
    {
        Py_DECREF(&SomeType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

setup.py

from setuptools import setup, Extension

setup(
    name="somemodule",
    version="0.1.0",
    ext_modules=[Extension("somemodule", ["main.c"])]
)

demo.py

import somemodule
somemodule.Some(12, 12, 12).__reduce__()

Here is the GDB output and backtrace:

GDB Output

$ gdb /home/xlurio/Playground/minimum-SIGSEGV/venv/bin/python -ex "run \"/home/xlurio/Playground/minimum-SIGSEGV/demo.py\""
Program received signal SIGSEGV, Segmentation fault.
0x000000000058da02 in PyUnicode_FromFormatV ()
(gdb) bt
#0  0x000000000058da02 in PyUnicode_FromFormatV ()
#1  0x000000000055cbaf in PyErr_Format ()
#2  0x00000000004d2924 in ?? ()
#3  0x00007ffff7fbe252 in PyInit_somemodule () at main.c:57
#4  0x0000000000685b4e in _PyImport_LoadDynamicModuleWithSpec ()
#5  0x0000000000686661 in ?? ()
#6  0x00000000005c52f0 in ?? ()
#7  0x00000000005f61c8 in PyVectorcall_Call ()
--Type <RET> for more, q to quit, c to continue without paging--
#8  0x0000000000571917 in _PyEval_EvalFrameDefault ()
#9  0x0000000000569cea in _PyEval_EvalCodeWithName ()
#10 0x00000000005f6a13 in _PyFunction_Vectorcall ()
#11 0x0000000000570ac2 in _PyEval_EvalFrameDefault ()
#12 0x00000000005f6836 in _PyFunction_Vectorcall ()
#13 0x000000000056bbdf in _PyEval_EvalFrameDefault ()
#14 0x00000000005f6836 in _PyFunction_Vectorcall ()
#15 0x000000000056b9fd in _PyEval_EvalFrameDefault ()
--Type <RET> for more, q to quit, c to continue without paging--
#16 0x00000000005f6836 in _PyFunction_Vectorcall ()
#17 0x000000000056b9fd in _PyEval_EvalFrameDefault ()
#18 0x00000000005f6836 in _PyFunction_Vectorcall ()
#19 0x000000000056b9fd in _PyEval_EvalFrameDefault ()
#20 0x00000000005f6836 in _PyFunction_Vectorcall ()
#21 0x00000000005f3c41 in ?? ()
#22 0x00000000005f40a8 in _PyObject_CallMethodIdObjArgs ()
#23 0x0000000000552f1c in PyImport_ImportModuleLevelObject ()
--Type <RET> for more, q to quit, c to continue without paging--
#24 0x000000000056ddd5 in _PyEval_EvalFrameDefault ()
#25 0x0000000000569cea in _PyEval_EvalCodeWithName ()
#26 0x000000000068e7b7 in PyEval_EvalCode ()
#27 0x0000000000680001 in ?? ()
#28 0x000000000068007f in ?? ()
#29 0x0000000000680121 in ?? ()
#30 0x0000000000680db7 in PyRun_SimpleFileExFlags ()
#31 0x00000000006b8122 in Py_RunMain ()
--Type <RET> for more, q to quit, c to continue without paging--
#32 0x00000000006b84ad in Py_BytesMain ()
#33 0x00007ffff7de4083 in __libc_start_main (main=0x4ef1e0 <main>, argc=2, argv=0x7fffffffd998, init=<optimized out>, 
    fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffd988) at ../csu/libc-start.c:308
#34 0x00000000005fb39e in _start ()

You need to end your PyMethodDef array with {NULL, NULL, 0, NULL}, so Python knows that it should stop iterating through it.

1 Like

Use “O” instead of “N” in Py_BuildValue(). “N” increases the reference count of the object after use, you should not do this with Py_None.

That was it!
I had forgotten that. Thank you so much!

According to the documentation N does the exactly opposite of that.

N (object) [PyObject *]
Same as O, except it doesn’t increment the reference count on the object. Useful when the object is created by a call to an object constructor in the argument list.

And it is used several times in Python C API when creating a tuple with a Py_None element. See the __reduce__ method for deque for example:

static PyObject *
deque_reduce(dequeobject *deque, PyObject *Py_UNUSED(ignored))
{
    PyObject *dict, *it;
    _Py_IDENTIFIER(__dict__);

    if (_PyObject_LookupAttrId((PyObject *)deque, &PyId___dict__, &dict) < 0) {
        return NULL;
    }
    if (dict == NULL) {
        dict = Py_None;
        Py_INCREF(dict);
    }

    it = PyObject_GetIter((PyObject *)deque);
    if (it == NULL) {
        Py_DECREF(dict);
        return NULL;
    }

    if (deque->maxlen < 0) {
        return Py_BuildValue("O()NN", Py_TYPE(deque), dict, it);
    }
    else {
        return Py_BuildValue("O(()n)NN", Py_TYPE(deque), deque->maxlen, dict, it);
    }
}