Python extension creates invalid pointers when manipulating large lists

I was able to implement the Fisher-Yates Shuffle function for python lists as an exercise to get used to the python extension. It works great for relatively small lists, unless I run the function multiple times.

Whenever the list size exceeds 100, I get all sorts of memory problems:

>>>import evosutil
>>> a=[i for i in range(100)]
>>> evosutil.shuffle(a)
>>> a
[52, 66, 0, 58, 41, 18, 50, 37, 81, 43, 74, 49, 90, 20, 63, 32, 89, 60, 2, 44, 3, 80, 15, 24, 22, 69, 86, 31, 56, 68, 34, 13, 38, 26, 14, 91, 73, 79, 39, 65, 5, 75, 84, 55, 7, 53, 93, 42, 40, 9, 51, 82, 29, 30, 99, 64, 33, 97, 27, 11, 6, 67, 16, 94, 95, 62, 57, 17, 78, 77, 71, 98, 72, 8, 88, 36, 85, 59, 21, 96, 23, 46, 10, 12, 48, 83, 4, 92, 45, 54, 1, 25, 19, 70, 35, 61, 47, 28, 87, 76]
>>> (Ctrl-D)
*** Error in `python3': free(): invalid next size (fast): 0x083fe680 ***

      

Or, trying to work with a list with 1000 elements:

*** Error in `python3': munmap_chunk(): invalid pointer: 0x083ff0e0 ***

      

Or

Segmentation fault (core dumped)

      

Here's my code for the module that is throwing the error:

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    PyObject* tmp=PyList_GetItem(list, i2);
    PyList_SetItem(list, i2, PyList_GetItem(list, i1));
    PyList_SetItem(list, i1, tmp);
}

//Naive Fisher–Yates shuffle
static PyObject* shuffle(PyObject* self, PyObject* args){
    PyObject* list;
    PyArg_ParseTuple(args,"O", &list);
    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::minstd_rand0 rand(seed);
    Py_ssize_t size = PyList_Size(list);
    for(int i=0; i<size;++i){
        int randIndex = rand()%size;
        _List_SwapItems(list, randIndex, i);
    }
    Py_RETURN_NONE;
}

      

It seems to me that I can solve this either with free () or with Py_DECREF (), but I cannot see where. I don't think I am creating any objects just by moving them. So where is the memory problem?

+3


source to share


2 answers


You need Py_XINCREF()

both objects before passing them to PyList_SetItem()

. Next, catch the special case when i1 == i2

:

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    if (i1 == i2) {
        return;
    }
    PyObject* obj1=PyList_GetItem(list, i1);
    PyObject* obj2=PyList_GetItem(list, i2);
    Py_XINCREF(obj1);
    Py_XINCREF(obj2);
    PyList_SetItem(list, i2, obj1);
    PyList_SetItem(list, i1, obj2);
}

      

PyList_GetItem()

returns a borrowed reference, that is, it does not return a INCREF

returned object. If you don't keep other references, the refcount will be 1

(since it only references a list). When you call PyList_SetItem(list, i2, ...)

, the list is the Py_XDECREF()

object previously stored in i2

(which you store in tmp

). At this point, the refcount is reached 0

and the object is freed. Oops.



Likewise, you cannot simply call PyList_SetItem(list, i, PyList_GetItem())

because it SetItem

steals the link you are passing. You do not own the link, however, this is an "old" list. Therefore, you also need Py_XINCREF

.

See the List API documentation for details .

As an additional suggestion, you might consider not programming directly against the Python extension API. It takes a lot of code to do something and it is very difficult to keep the refcounts correct. By now, there are several other ways Python interacts with C or C ++. CFFI seems to be the low-level interface that the Python ecosystem will standardize. SIP and SWIG may offer better support for C ++. For a SIP example, see this answer .

+3


source


There are more problems with your extension function outside of reference counting errors, more of them below:


While PyList_SetItem

correct reference counting is the preferred way, the (ugly) option is to use a macro PyList_SET_ITEM

that goes away with the INCREF:

void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o)

Macro formation PyList_SetItem()

without error checking. This is usually only used to populate new listings that do not have previous content.

Note

This macro "steals" a reference to an element and, unlike PyList_SetItem()

, does not discard a reference to any element that is being replaced; any link listed in position i

will be leaked.

This way, it PyList_SET_ITEM

does not increase or decrease any reference counters that are appropriate for us, since initially and finally the items are in the same list.

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    PyObject* tmp = PyList_GET_ITEM(list, i2);
    PyList_SET_ITEM(list, i2, PyList_GET_ITEM(list, i1));
    PyList_SET_ITEM(list, i1, tmp);
}

      

Note that this does not do any error checking at all, so you need to make sure your index is within the limits (which the loop handles for

).




There is another bad problem in your code that is not discussed yet - the complete lack of error checking. For example, when passing in an object that is not a list, you must raise TypeError

. Now the code will be PyList_Size

, by returning -1 and setting an inner exception, this could lead to erroneous behavior for all future C extensions:

Likewise, it PyArg_ParseTuple

may fail if the wrong number of arguments is passed to , so you should check its return value; in this case it list

can be uninitialized and your code will have full undefined behavior.

The C-API documentation states the following :

If a function should fail because some function called by it failed, it usually does not set an error indicator; the function she was calling had already installed it. It is responsible for handling errors and clearing an exception or returning after clearing any resources (such as object references or memory allocations); it shouldn't proceed normally unless it's ready to handle the error. If returning due to an error, it is important to indicate to the caller that an error has been fixed. If an error is not handled or carefully additional calls in the Python / C API may not be as intended and may fail in mysterious ways.

So here's the correct way to write your extension function:

static PyObject* shuffle(PyObject* self, PyObject* args){
    PyObject* list;
    if (! PyArg_ParseTuple(args, "O", &list)) {
        // PyArg_ParseTuple set the proper exception
        return NULL;
    }

    if (! PyList_Check(list)) {
        PyErr_SetString(PyExc_TypeError,
            "bad argument to shuffle; list expected");
        return NULL;
    }

    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::minstd_rand0 rand(seed);

    Py_ssize_t size = PyList_Size(list);
    for(int i=0; i<size;++i){
        int randIndex = rand()%size;
        _List_SwapItems(list, randIndex, i);
    }
    Py_RETURN_NONE;
}

      

+3


source







All Articles